Skip to main content

rust_sanitize/processor/
registry.rs

1//! Processor registry — discovers and dispatches structured processors.
2//!
3//! The [`ProcessorRegistry`] holds a set of registered [`Processor`]
4//! implementations and provides methods to:
5//!
6//! 1. Look up a processor by name.
7//! 2. Auto-detect a processor for given content + profile.
8//! 3. Process content using a matching processor, falling back to `None`
9//!    if no processor matches (caller can then use the streaming scanner).
10
11use super::{FileTypeProfile, Processor};
12use crate::error::Result;
13use crate::store::MappingStore;
14use std::collections::HashMap;
15use std::sync::Arc;
16
17/// Registry of structured processors.
18///
19/// Thread-safe (processors are `Arc<dyn Processor>`) and can be shared
20/// across threads via `Arc<ProcessorRegistry>`.
21pub struct ProcessorRegistry {
22    /// Processors indexed by name.
23    processors: HashMap<String, Arc<dyn Processor>>,
24}
25
26impl ProcessorRegistry {
27    /// Create an empty registry.
28    #[must_use]
29    pub fn new() -> Self {
30        Self {
31            processors: HashMap::new(),
32        }
33    }
34
35    /// Create a registry pre-populated with all built-in processors.
36    #[must_use]
37    pub fn with_builtins() -> Self {
38        let mut reg = Self::new();
39        // Registered under "key_value" (canonical, from name()) and "key-value" (hyphen alias)
40        // so profile files can use either naming convention.
41        reg.register_with_alias(Arc::new(super::key_value::KeyValueProcessor), "key-value");
42        reg.register(Arc::new(super::json_proc::JsonProcessor));
43        reg.register(Arc::new(super::jsonl_proc::JsonLinesProcessor));
44        reg.register(Arc::new(super::yaml_proc::YamlProcessor));
45        reg.register(Arc::new(super::xml_proc::XmlProcessor));
46        reg.register(Arc::new(super::csv_proc::CsvProcessor));
47        reg.register(Arc::new(super::toml_proc::TomlProcessor));
48        reg.register(Arc::new(super::env_proc::EnvProcessor));
49        reg.register(Arc::new(super::ini_proc::IniProcessor));
50        reg.register(Arc::new(super::log_line::LogLineProcessor::new()));
51        reg
52    }
53
54    /// Register a processor. Overwrites any existing processor with the
55    /// same name.
56    pub fn register(&mut self, processor: Arc<dyn Processor>) {
57        self.processors
58            .insert(processor.name().to_string(), processor);
59    }
60
61    /// Register a processor under its canonical name and an additional alias.
62    pub fn register_with_alias(&mut self, processor: Arc<dyn Processor>, alias: &str) {
63        self.processors
64            .insert(alias.to_string(), Arc::clone(&processor));
65        self.processors
66            .insert(processor.name().to_string(), processor);
67    }
68
69    /// Look up a processor by its name.
70    pub fn get(&self, name: &str) -> Option<&Arc<dyn Processor>> {
71        self.processors.get(name)
72    }
73
74    /// List all registered processor names.
75    pub fn names(&self) -> Vec<&str> {
76        self.processors.keys().map(|s| s.as_str()).collect()
77    }
78
79    /// Number of registered processors.
80    #[must_use]
81    pub fn len(&self) -> usize {
82        self.processors.len()
83    }
84
85    /// Whether the registry is empty.
86    #[must_use]
87    pub fn is_empty(&self) -> bool {
88        self.processors.is_empty()
89    }
90
91    /// Find a processor that can handle the given content + profile.
92    ///
93    /// 1. If the profile names a specific processor, look it up directly.
94    /// 2. Otherwise, iterate all processors and return the first whose
95    ///    `can_handle` returns `true`.
96    ///
97    /// Returns `None` if no processor matches (caller should fall back
98    /// to the streaming scanner).
99    pub fn find_processor(
100        &self,
101        content: &[u8],
102        profile: &FileTypeProfile,
103    ) -> Option<&Arc<dyn Processor>> {
104        // Direct lookup by profile's processor name.
105        if let Some(proc) = self.processors.get(&profile.processor) {
106            if proc.can_handle(content, profile) {
107                return Some(proc);
108            }
109        }
110
111        // Auto-detect: first matching processor.
112        self.processors
113            .values()
114            .find(|proc| proc.can_handle(content, profile))
115    }
116
117    /// Process content using the matching processor.
118    ///
119    /// Returns `Ok(Some(output))` if a processor matched and succeeded,
120    /// `Ok(None)` if no processor matches (caller should fall back),
121    /// or `Err(...)` if processing failed.
122    ///
123    /// # Errors
124    ///
125    /// Returns the underlying processor's error if processing fails.
126    pub fn process(
127        &self,
128        content: &[u8],
129        profile: &FileTypeProfile,
130        store: &MappingStore,
131    ) -> Result<Option<Vec<u8>>> {
132        match self.find_processor(content, profile) {
133            Some(proc) => {
134                let output = proc.process(content, profile, store)?;
135                Ok(Some(output))
136            }
137            None => Ok(None),
138        }
139    }
140}
141
142impl Default for ProcessorRegistry {
143    fn default() -> Self {
144        Self::with_builtins()
145    }
146}
147
148#[cfg(test)]
149mod tests {
150    use super::*;
151    use crate::category::Category;
152    use crate::generator::HmacGenerator;
153    use crate::processor::profile::{FieldRule, FileTypeProfile};
154    use std::sync::Arc;
155
156    fn make_store() -> MappingStore {
157        let gen = Arc::new(HmacGenerator::new([42u8; 32]));
158        MappingStore::new(gen, None)
159    }
160
161    #[test]
162    fn new_registry_is_empty() {
163        let reg = ProcessorRegistry::new();
164        assert!(reg.is_empty());
165        assert_eq!(reg.len(), 0);
166    }
167
168    #[test]
169    fn with_builtins_registers_known_processors() {
170        let reg = ProcessorRegistry::with_builtins();
171        assert!(!reg.is_empty());
172        let names = reg.names();
173        for expected in &["json", "yaml", "xml", "csv", "toml", "jsonl"] {
174            assert!(names.contains(expected), "missing processor: {expected}");
175        }
176    }
177
178    #[test]
179    fn register_and_get_roundtrip() {
180        let mut reg = ProcessorRegistry::new();
181        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
182        assert!(reg.get("json").is_some());
183        assert!(reg.get("xml").is_none());
184    }
185
186    #[test]
187    fn register_overwrites_existing() {
188        let mut reg = ProcessorRegistry::new();
189        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
190        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
191        assert_eq!(reg.len(), 1);
192    }
193
194    #[test]
195    fn names_lists_all_registered() {
196        let mut reg = ProcessorRegistry::new();
197        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
198        reg.register(Arc::new(crate::processor::yaml_proc::YamlProcessor));
199        let names = reg.names();
200        assert_eq!(names.len(), 2);
201        assert!(names.contains(&"json"));
202        assert!(names.contains(&"yaml"));
203    }
204
205    #[test]
206    fn find_processor_by_profile_name() {
207        let reg = ProcessorRegistry::with_builtins();
208        let profile = FileTypeProfile::new("json", vec![]).with_extension(".json");
209        let content = b"{}";
210        assert!(reg.find_processor(content, &profile).is_some());
211    }
212
213    #[test]
214    fn find_processor_returns_none_for_unrecognised_content() {
215        let reg = ProcessorRegistry::new(); // empty — nothing registered
216        let profile = FileTypeProfile::new("json", vec![]).with_extension(".json");
217        assert!(reg.find_processor(b"{}", &profile).is_none());
218    }
219
220    #[test]
221    fn process_returns_some_for_matching_content() {
222        let reg = ProcessorRegistry::with_builtins();
223        let store = make_store();
224        let profile = FileTypeProfile::new(
225            "json",
226            vec![FieldRule::new("*.secret").with_category(Category::Custom("s".into()))],
227        )
228        .with_extension(".json");
229        let result = reg
230            .process(br#"{"secret":"abc"}"#, &profile, &store)
231            .unwrap();
232        assert!(result.is_some());
233    }
234
235    #[test]
236    fn process_returns_none_when_no_processor_matches() {
237        let reg = ProcessorRegistry::new(); // empty
238        let store = make_store();
239        let profile = FileTypeProfile::new("json", vec![]).with_extension(".json");
240        let result = reg.process(b"{}", &profile, &store).unwrap();
241        assert!(result.is_none());
242    }
243
244    #[test]
245    fn default_impl_gives_builtins() {
246        let reg = ProcessorRegistry::default();
247        assert!(reg.get("json").is_some());
248    }
249}