crush_core/plugin/
registry.rs1use crate::error::{PluginError, Result};
7use crate::plugin::{CompressionAlgorithm, PluginMetadata, COMPRESSION_ALGORITHMS};
8use std::collections::HashMap;
9use std::sync::RwLock;
10
11static PLUGIN_REGISTRY: RwLock<Option<PluginRegistry>> = RwLock::new(None);
16
17struct PluginRegistry {
19 plugins: HashMap<[u8; 4], &'static dyn CompressionAlgorithm>,
21 initialized: bool,
23}
24
25impl PluginRegistry {
26 fn new() -> Self {
28 Self {
29 plugins: HashMap::new(),
30 initialized: false,
31 }
32 }
33
34 fn clear(&mut self) {
36 self.plugins.clear();
37 self.initialized = false;
38 }
39
40 fn register(&mut self, plugin: &'static dyn CompressionAlgorithm) -> Result<()> {
42 let metadata = plugin.metadata();
43
44 if metadata.name.is_empty() {
46 return Err(
47 PluginError::InvalidMetadata("Plugin name cannot be empty".to_string()).into(),
48 );
49 }
50
51 if metadata.version.is_empty() {
52 return Err(
53 PluginError::InvalidMetadata("Plugin version cannot be empty".to_string()).into(),
54 );
55 }
56
57 if metadata.throughput <= 0.0 {
58 return Err(PluginError::InvalidMetadata(format!(
59 "Plugin {} has invalid throughput: {}",
60 metadata.name, metadata.throughput
61 ))
62 .into());
63 }
64
65 if metadata.compression_ratio <= 0.0 || metadata.compression_ratio > 1.0 {
66 return Err(PluginError::InvalidMetadata(format!(
67 "Plugin {} has invalid compression ratio: {}",
68 metadata.name, metadata.compression_ratio
69 ))
70 .into());
71 }
72
73 if let Some(existing) = self.plugins.get(&metadata.magic_number) {
75 let existing_metadata = existing.metadata();
76 eprintln!(
77 "Warning: Duplicate magic number {:02X?} detected. \
78 Plugin '{}' conflicts with '{}'. \
79 Using first-registered plugin '{}'.",
80 metadata.magic_number,
81 metadata.name,
82 existing_metadata.name,
83 existing_metadata.name
84 );
85 return Ok(()); }
87
88 self.plugins.insert(metadata.magic_number, plugin);
90
91 Ok(())
92 }
93
94 fn list(&self) -> Vec<PluginMetadata> {
96 self.plugins
97 .values()
98 .map(|plugin| plugin.metadata())
99 .collect()
100 }
101
102 fn get(&self, magic: [u8; 4]) -> Option<&'static dyn CompressionAlgorithm> {
104 self.plugins.get(&magic).copied()
105 }
106}
107
108pub fn init_plugins() -> Result<()> {
134 let mut guard = PLUGIN_REGISTRY
135 .write()
136 .map_err(|_| PluginError::OperationFailed("Failed to acquire registry lock".to_string()))?;
137
138 let registry = guard.get_or_insert_with(PluginRegistry::new);
140
141 if registry.initialized {
143 registry.clear();
144 }
145
146 for &plugin in COMPRESSION_ALGORITHMS {
148 registry.register(plugin)?;
150 }
151
152 registry.initialized = true;
153
154 Ok(())
155}
156
157#[must_use]
179pub fn list_plugins() -> Vec<PluginMetadata> {
180 PLUGIN_REGISTRY
181 .read()
182 .ok()
183 .and_then(|guard| guard.as_ref().map(PluginRegistry::list))
184 .unwrap_or_default()
185}
186
187pub(crate) fn get_plugin_by_magic(magic: [u8; 4]) -> Option<&'static dyn CompressionAlgorithm> {
191 PLUGIN_REGISTRY
192 .read()
193 .ok()
194 .and_then(|guard| guard.as_ref().and_then(|registry| registry.get(magic)))
195}
196
197pub(crate) fn get_default_plugin() -> Option<&'static dyn CompressionAlgorithm> {
201 get_plugin_by_magic([0x43, 0x52, 0x01, 0x00])
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 macro_rules! make_invalid_plugin {
214 ($name:expr, $version:expr, $magic:expr, $throughput:expr, $ratio:expr) => {{
215 struct InvalidPlugin;
216 impl CompressionAlgorithm for InvalidPlugin {
217 fn name(&self) -> &'static str {
218 "test_invalid"
219 }
220 fn metadata(&self) -> PluginMetadata {
221 PluginMetadata {
222 name: $name,
223 version: $version,
224 magic_number: $magic,
225 throughput: $throughput,
226 compression_ratio: $ratio,
227 description: "Test",
228 }
229 }
230 fn compress(
231 &self,
232 _input: &[u8],
233 _cancel_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
234 ) -> crate::error::Result<Vec<u8>> {
235 Ok(vec![])
236 }
237 fn decompress(
238 &self,
239 _input: &[u8],
240 _cancel_flag: std::sync::Arc<std::sync::atomic::AtomicBool>,
241 ) -> crate::error::Result<Vec<u8>> {
242 Ok(vec![])
243 }
244 fn detect(&self, _file_header: &[u8]) -> bool {
245 false
246 }
247 }
248 let plugin: &'static dyn CompressionAlgorithm = Box::leak(Box::new(InvalidPlugin));
249 plugin
250 }};
251 }
252
253 #[test]
254 #[allow(clippy::unwrap_used)]
255 fn test_init_plugins() {
256 init_plugins().unwrap();
257
258 let plugins = list_plugins();
259 assert!(
260 !plugins.is_empty(),
261 "Should discover at least DEFLATE plugin"
262 );
263 }
264
265 #[test]
266 #[allow(clippy::unwrap_used)]
267 fn test_get_default_plugin() {
268 init_plugins().unwrap();
269
270 let plugin = get_default_plugin();
271 assert!(
272 plugin.is_some(),
273 "Default DEFLATE plugin should be available"
274 );
275
276 let plugin = plugin.unwrap();
277 assert_eq!(plugin.name(), "deflate");
278 }
279
280 #[test]
281 #[allow(clippy::unwrap_used)]
282 fn test_reinitialization() {
283 init_plugins().unwrap();
284 let count1 = list_plugins().len();
285
286 init_plugins().unwrap();
287 let count2 = list_plugins().len();
288
289 assert_eq!(
290 count1, count2,
291 "Re-initialization should maintain plugin count"
292 );
293 }
294
295 #[test]
296 #[allow(clippy::unwrap_used)]
297 fn test_registry_validation_empty_name() {
298 let mut registry = PluginRegistry::new();
299 let plugin = make_invalid_plugin!("", "1.0.0", [0x43, 0x52, 0x01, 0xFF], 100.0, 0.5);
300 let result = registry.register(plugin);
301
302 assert!(result.is_err());
303 let err_msg = result.unwrap_err().to_string();
304 assert!(err_msg.contains("name cannot be empty"));
305 }
306
307 #[test]
308 #[allow(clippy::unwrap_used)]
309 fn test_registry_validation_empty_version() {
310 let mut registry = PluginRegistry::new();
311 let plugin = make_invalid_plugin!("test", "", [0x43, 0x52, 0x01, 0xFE], 100.0, 0.5);
312 let result = registry.register(plugin);
313
314 assert!(result.is_err());
315 let err_msg = result.unwrap_err().to_string();
316 assert!(err_msg.contains("version cannot be empty"));
317 }
318
319 #[test]
320 #[allow(clippy::unwrap_used)]
321 fn test_registry_validation_invalid_throughput() {
322 let mut registry = PluginRegistry::new();
323 let plugin = make_invalid_plugin!("test", "1.0.0", [0x43, 0x52, 0x01, 0xFD], -10.0, 0.5);
325 let result = registry.register(plugin);
326
327 assert!(result.is_err());
328 let err_msg = result.unwrap_err().to_string();
329 assert!(err_msg.contains("invalid throughput"));
330 }
331
332 #[test]
333 #[allow(clippy::unwrap_used)]
334 fn test_registry_validation_invalid_compression_ratio() {
335 let mut registry = PluginRegistry::new();
336 let plugin = make_invalid_plugin!("test", "1.0.0", [0x43, 0x52, 0x01, 0xFC], 100.0, 1.5);
338 let result = registry.register(plugin);
339
340 assert!(result.is_err());
341 let err_msg = result.unwrap_err().to_string();
342 assert!(err_msg.contains("invalid compression ratio"));
343 }
344
345 #[test]
346 #[allow(clippy::unwrap_used)]
347 fn test_get_plugin_by_magic() {
348 init_plugins().unwrap();
349
350 let plugin = get_plugin_by_magic([0x43, 0x52, 0x01, 0x00]);
352 assert!(plugin.is_some());
353 assert_eq!(plugin.unwrap().name(), "deflate");
354
355 let plugin = get_plugin_by_magic([0xFF, 0xFF, 0xFF, 0xFF]);
357 assert!(plugin.is_none());
358 }
359
360 #[test]
361 fn test_list_plugins_before_init() {
362 let plugins = list_plugins();
365 let _ = plugins.len();
368 }
369
370 #[test]
371 #[allow(clippy::unwrap_used)]
372 fn test_registry_clear() {
373 init_plugins().unwrap();
374 let count1 = list_plugins().len();
375 assert!(count1 > 0);
376
377 init_plugins().unwrap();
379 let count2 = list_plugins().len();
380 assert_eq!(count1, count2);
381 }
382}