Skip to main content

termivar_scanner/plugin/
registry.rs

1use dashmap::{mapref::entry::Entry, DashMap};
2use std::{
3    panic::AssertUnwindSafe,
4    sync::{Arc, Mutex, MutexGuard},
5    time::{SystemTime, UNIX_EPOCH},
6};
7
8use super::{
9    limits::{
10        invalid_config, validate_identifier, validate_text, PluginConfig, MAX_PLUGIN_ID_BYTES,
11    },
12    metadata::{PluginDescriptor, PluginMetadata},
13    Plugin, PluginCategory, PluginError, PLUGIN_API_VERSION,
14};
15
16pub(super) struct PluginStats {
17    state: Mutex<PluginStatsState>,
18}
19
20#[derive(Default)]
21struct PluginStatsState {
22    execution_count: u64,
23    success_count: u64,
24    error_count: u64,
25    active_invocations: u64,
26}
27
28impl PluginStats {
29    pub(super) fn acquire_invocation(
30        self: &Arc<Self>,
31    ) -> Result<PluginInvocationLease, PluginError> {
32        let mut state = lock_stats(&self.state);
33        state.active_invocations = state
34            .active_invocations
35            .checked_add(1)
36            .ok_or(PluginError::HostStateUnavailable)?;
37        Ok(PluginInvocationLease {
38            stats: self.clone(),
39        })
40    }
41
42    fn release_invocation(&self) {
43        let mut state = lock_stats(&self.state);
44        state.active_invocations = state.active_invocations.saturating_sub(1);
45    }
46
47    fn has_active_invocation(&self) -> bool {
48        lock_stats(&self.state).active_invocations != 0
49    }
50
51    pub(super) fn record_execution(&self) {
52        let mut state = lock_stats(&self.state);
53        state.execution_count = state.execution_count.saturating_add(1);
54    }
55
56    pub(super) fn record_success(&self) {
57        let mut state = lock_stats(&self.state);
58        state.success_count = state.success_count.saturating_add(1);
59    }
60
61    pub(super) fn record_error(&self) {
62        let mut state = lock_stats(&self.state);
63        state.error_count = state.error_count.saturating_add(1);
64    }
65
66    fn snapshot(&self) -> (u64, u64, u64) {
67        let state = lock_stats(&self.state);
68        (
69            state.execution_count,
70            state.success_count,
71            state.error_count,
72        )
73    }
74}
75
76pub(super) struct PluginInvocationLease {
77    stats: Arc<PluginStats>,
78}
79
80impl Drop for PluginInvocationLease {
81    fn drop(&mut self) {
82        self.stats.release_invocation();
83    }
84}
85
86fn lock_stats(state: &Mutex<PluginStatsState>) -> MutexGuard<'_, PluginStatsState> {
87    match state.lock() {
88        Ok(guard) => guard,
89        Err(poisoned) => poisoned.into_inner(),
90    }
91}
92
93pub(super) struct PluginEntry {
94    pub(super) plugin: Arc<dyn Plugin>,
95    pub(super) config: PluginConfig,
96    descriptor: PluginDescriptor,
97    pub(super) stats: Arc<PluginStats>,
98}
99
100#[derive(Default)]
101pub struct PluginRegistry {
102    pub(super) entries: DashMap<String, PluginEntry>,
103}
104
105impl PluginRegistry {
106    /// Creates an empty registry.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Registers one plugin and host configuration without replacement.
112    pub fn register(
113        &self,
114        plugin: Arc<dyn Plugin>,
115        config: PluginConfig,
116    ) -> Result<(), PluginError> {
117        self.register_at(plugin, config, SystemTime::now())
118    }
119
120    pub(super) fn register_at(
121        &self,
122        plugin: Arc<dyn Plugin>,
123        config: PluginConfig,
124        now: SystemTime,
125    ) -> Result<(), PluginError> {
126        let descriptor = std::panic::catch_unwind(AssertUnwindSafe(|| {
127            let descriptor = plugin_descriptor(plugin.as_ref(), now)?;
128            validate_plugin_descriptor(&descriptor)?;
129            validate_api_version(&descriptor.api_version)?;
130            plugin
131                .validate()
132                .map_err(|_| invalid_config("plugin validation failed"))?;
133            Ok(descriptor)
134        }))
135        .map_err(|_| PluginError::Panicked)??;
136        let id = descriptor.id.clone();
137        let entry = PluginEntry {
138            plugin,
139            config,
140            descriptor,
141            stats: Arc::new(PluginStats {
142                state: Mutex::new(PluginStatsState::default()),
143            }),
144        };
145        match self.entries.entry(id) {
146            Entry::Vacant(slot) => {
147                slot.insert(entry);
148                Ok(())
149            },
150            Entry::Occupied(_) => Err(PluginError::DuplicateId),
151        }
152    }
153
154    /// Removes one plugin and its inseparable configuration/metadata entry.
155    pub fn unregister(&self, plugin_id: &str) -> Result<(), PluginError> {
156        if plugin_id.is_empty() || plugin_id.len() > MAX_PLUGIN_ID_BYTES {
157            return Err(PluginError::NotFound);
158        }
159        match self.entries.entry(plugin_id.to_owned()) {
160            Entry::Occupied(entry) if entry.get().stats.has_active_invocation() => {
161                Err(PluginError::InUse)
162            },
163            Entry::Occupied(entry) => {
164                entry.remove();
165                Ok(())
166            },
167            Entry::Vacant(_) => Err(PluginError::NotFound),
168        }
169    }
170
171    /// Returns the registered plugin trait object.
172    pub fn get(&self, plugin_id: &str) -> Option<Arc<dyn Plugin>> {
173        self.entries
174            .get(plugin_id)
175            .map(|entry| entry.plugin.clone())
176    }
177
178    /// Returns one consistent metadata snapshot.
179    pub fn get_metadata(&self, plugin_id: &str) -> Option<PluginMetadata> {
180        self.entries
181            .get(plugin_id)
182            .map(|entry| metadata_snapshot(&entry))
183    }
184
185    /// Returns one consistent host-configuration snapshot.
186    pub fn get_config(&self, plugin_id: &str) -> Option<PluginConfig> {
187        self.entries
188            .get(plugin_id)
189            .map(|entry| entry.config.clone())
190    }
191
192    /// Replaces host policy atomically for future invocations.
193    pub fn update_config(&self, plugin_id: &str, config: PluginConfig) -> Result<(), PluginError> {
194        let mut entry = self
195            .entries
196            .get_mut(plugin_id)
197            .ok_or(PluginError::NotFound)?;
198        entry.config = config;
199        Ok(())
200    }
201}
202
203impl PluginRegistry {
204    /// Lists consistent metadata snapshots in plugin-ID order.
205    pub fn list_all(&self) -> Vec<PluginMetadata> {
206        let mut metadata: Vec<_> = self
207            .entries
208            .iter()
209            .map(|entry| metadata_snapshot(&entry))
210            .collect();
211        metadata.sort_by(|left, right| left.id.cmp(&right.id));
212        metadata
213    }
214
215    /// Lists consistent metadata snapshots for one category.
216    pub fn list_by_category(&self, category: PluginCategory) -> Vec<PluginMetadata> {
217        self.list_all()
218            .into_iter()
219            .filter(|metadata| metadata.category == category)
220            .collect()
221    }
222
223    /// Registered plugin count.
224    pub fn count(&self) -> usize {
225        self.entries.len()
226    }
227}
228
229fn metadata_snapshot(entry: &PluginEntry) -> PluginMetadata {
230    let (execution_count, success_count, error_count) = entry.stats.snapshot();
231    PluginMetadata {
232        id: entry.descriptor.id.clone(),
233        name: entry.descriptor.name.clone(),
234        version: entry.descriptor.version.clone(),
235        description: entry.descriptor.description.clone(),
236        author: entry.descriptor.author.clone(),
237        category: entry.descriptor.category,
238        api_version: entry.descriptor.api_version.clone(),
239        enabled: entry.config.enabled,
240        loaded_at: entry.descriptor.loaded_at,
241        execution_count,
242        success_count,
243        error_count,
244    }
245}
246
247fn plugin_descriptor(
248    plugin: &dyn Plugin,
249    now: SystemTime,
250) -> Result<PluginDescriptor, PluginError> {
251    let loaded_at = now
252        .duration_since(UNIX_EPOCH)
253        .map_err(|_| PluginError::ClockBeforeUnixEpoch)?
254        .as_secs();
255    Ok(PluginDescriptor {
256        id: plugin.id().to_owned(),
257        name: plugin.name().to_owned(),
258        version: plugin.version().to_owned(),
259        description: plugin.description().to_owned(),
260        author: plugin.author().to_owned(),
261        category: plugin.category(),
262        api_version: plugin.api_version().to_owned(),
263        loaded_at,
264    })
265}
266
267fn validate_plugin_descriptor(descriptor: &PluginDescriptor) -> Result<(), PluginError> {
268    validate_identifier(&descriptor.id, "plugin id", MAX_PLUGIN_ID_BYTES)?;
269    validate_text(&descriptor.name, "plugin name")?;
270    validate_identifier(&descriptor.version, "plugin version", MAX_PLUGIN_ID_BYTES)?;
271    validate_text(&descriptor.description, "plugin description")?;
272    validate_text(&descriptor.author, "plugin author")?;
273    Ok(())
274}
275
276pub(super) fn validate_api_version(actual: &str) -> Result<(), PluginError> {
277    fn line(version: &str) -> Option<(u64, u64)> {
278        let mut parts = version.split('.');
279        let major = parts.next()?.parse().ok()?;
280        let minor = parts.next()?.parse().ok()?;
281        let _patch: u64 = parts.next()?.parse().ok()?;
282        if parts.next().is_some() {
283            return None;
284        }
285        Some((major, minor))
286    }
287    let actual_line = line(actual);
288    if actual_line.is_some() && actual_line == line(PLUGIN_API_VERSION) {
289        Ok(())
290    } else {
291        Err(PluginError::IncompatibleApiVersion {
292            expected: PLUGIN_API_VERSION.to_owned(),
293            actual: if actual_line.is_some() && actual.len() <= 32 {
294                actual.to_owned()
295            } else {
296                "[invalid]".to_owned()
297            },
298        })
299    }
300}