Skip to main content

scour_secrets/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        #[cfg(feature = "structured")]
46        reg.register(Arc::new(super::xml_proc::XmlProcessor));
47        #[cfg(feature = "structured")]
48        reg.register(Arc::new(super::csv_proc::CsvProcessor));
49        reg.register(Arc::new(super::toml_proc::TomlProcessor));
50        reg.register(Arc::new(super::env_proc::EnvProcessor));
51        reg.register(Arc::new(super::ini_proc::IniProcessor));
52        reg.register(Arc::new(super::log_line::LogLineProcessor::new()));
53        reg.register(Arc::new(super::command_output::CommandOutputProcessor));
54        reg.register(Arc::new(super::columns::ColumnsProcessor));
55        reg
56    }
57
58    /// Register a processor. Overwrites any existing processor with the
59    /// same name.
60    pub fn register(&mut self, processor: Arc<dyn Processor>) {
61        self.processors
62            .insert(processor.name().to_string(), processor);
63    }
64
65    /// Register a processor under its canonical name and an additional alias.
66    pub fn register_with_alias(&mut self, processor: Arc<dyn Processor>, alias: &str) {
67        self.processors
68            .insert(alias.to_string(), Arc::clone(&processor));
69        self.processors
70            .insert(processor.name().to_string(), processor);
71    }
72
73    /// Look up a processor by its name.
74    pub fn get(&self, name: &str) -> Option<&Arc<dyn Processor>> {
75        self.processors.get(name)
76    }
77
78    /// List all registered processor names.
79    pub fn names(&self) -> Vec<&str> {
80        self.processors.keys().map(|s| s.as_str()).collect()
81    }
82
83    /// Number of registered processors.
84    #[must_use]
85    pub fn len(&self) -> usize {
86        self.processors.len()
87    }
88
89    /// Whether the registry is empty.
90    #[must_use]
91    pub fn is_empty(&self) -> bool {
92        self.processors.is_empty()
93    }
94
95    /// Find a processor that can handle the given content + profile.
96    ///
97    /// 1. If the profile names a specific processor, look it up directly.
98    /// 2. Otherwise, iterate all processors and return the first whose
99    ///    `can_handle` returns `true`.
100    ///
101    /// Returns `None` if no processor matches (caller should fall back
102    /// to the streaming scanner).
103    pub fn find_processor(
104        &self,
105        content: &[u8],
106        profile: &FileTypeProfile,
107    ) -> Option<&Arc<dyn Processor>> {
108        // Direct lookup by profile's processor name.
109        if let Some(proc) = self.processors.get(&profile.processor) {
110            if proc.can_handle(content, profile) {
111                return Some(proc);
112            }
113        }
114
115        // Auto-detect: first matching processor.
116        self.processors
117            .values()
118            .find(|proc| proc.can_handle(content, profile))
119    }
120
121    /// Process content using the matching processor.
122    ///
123    /// Returns `Ok(Some(output))` if a processor matched and succeeded,
124    /// `Ok(None)` if no processor matches (caller should fall back),
125    /// or `Err(...)` if processing failed.
126    ///
127    /// # Errors
128    ///
129    /// Returns the underlying processor's error if processing fails.
130    pub fn process(
131        &self,
132        content: &[u8],
133        profile: &FileTypeProfile,
134        store: &MappingStore,
135    ) -> Result<Option<Vec<u8>>> {
136        match self.find_processor(content, profile) {
137            Some(proc) => {
138                let output = proc.process(content, profile, store)?;
139                Ok(Some(output))
140            }
141            None => Ok(None),
142        }
143    }
144
145    /// Span-based structured processing: returns the edited bytes when the
146    /// matching processor supports [`Processor::process_to_edits`], or `None`
147    /// when there is no matching processor or it does not support span editing
148    /// (the caller then falls back to [`Self::process`] + the scanner).
149    ///
150    /// # Errors
151    ///
152    /// Propagates parse/replacement errors from the processor.
153    pub fn process_to_edits(
154        &self,
155        content: &[u8],
156        profile: &FileTypeProfile,
157        store: &MappingStore,
158    ) -> Result<Option<(Vec<u8>, usize)>> {
159        match self.find_processor(content, profile) {
160            Some(proc) => match proc.process_to_edits(content, profile, store)? {
161                Some(edits) => {
162                    let count = edits.len();
163                    Ok(Some((super::apply_edits(content, edits), count)))
164                }
165                None => Ok(None),
166            },
167            None => Ok(None),
168        }
169    }
170}
171
172impl Default for ProcessorRegistry {
173    fn default() -> Self {
174        Self::with_builtins()
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181    use crate::category::Category;
182    use crate::generator::HmacGenerator;
183    use crate::processor::profile::{FieldRule, FileTypeProfile};
184    use std::sync::Arc;
185
186    fn make_store() -> MappingStore {
187        let gen = Arc::new(HmacGenerator::new([42u8; 32]));
188        MappingStore::new(gen, None)
189    }
190
191    #[test]
192    fn new_registry_is_empty() {
193        let reg = ProcessorRegistry::new();
194        assert!(reg.is_empty());
195        assert_eq!(reg.len(), 0);
196    }
197
198    #[test]
199    fn with_builtins_registers_known_processors() {
200        let reg = ProcessorRegistry::with_builtins();
201        assert!(!reg.is_empty());
202        let names = reg.names();
203        for expected in &["json", "yaml", "xml", "csv", "toml", "jsonl"] {
204            assert!(names.contains(expected), "missing processor: {expected}");
205        }
206    }
207
208    #[test]
209    fn register_and_get_roundtrip() {
210        let mut reg = ProcessorRegistry::new();
211        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
212        assert!(reg.get("json").is_some());
213        assert!(reg.get("xml").is_none());
214    }
215
216    #[test]
217    fn register_overwrites_existing() {
218        let mut reg = ProcessorRegistry::new();
219        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
220        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
221        assert_eq!(reg.len(), 1);
222    }
223
224    #[test]
225    fn names_lists_all_registered() {
226        let mut reg = ProcessorRegistry::new();
227        reg.register(Arc::new(crate::processor::json_proc::JsonProcessor));
228        reg.register(Arc::new(crate::processor::yaml_proc::YamlProcessor));
229        let names = reg.names();
230        assert_eq!(names.len(), 2);
231        assert!(names.contains(&"json"));
232        assert!(names.contains(&"yaml"));
233    }
234
235    #[test]
236    fn find_processor_by_profile_name() {
237        let reg = ProcessorRegistry::with_builtins();
238        let profile = FileTypeProfile::new("json", vec![]).with_extension(".json");
239        let content = b"{}";
240        assert!(reg.find_processor(content, &profile).is_some());
241    }
242
243    #[test]
244    fn find_processor_returns_none_for_unrecognised_content() {
245        let reg = ProcessorRegistry::new(); // empty — nothing registered
246        let profile = FileTypeProfile::new("json", vec![]).with_extension(".json");
247        assert!(reg.find_processor(b"{}", &profile).is_none());
248    }
249
250    #[test]
251    fn process_returns_some_for_matching_content() {
252        let reg = ProcessorRegistry::with_builtins();
253        let store = make_store();
254        let profile = FileTypeProfile::new(
255            "json",
256            vec![FieldRule::new("*.secret").with_category(Category::Custom("s".into()))],
257        )
258        .with_extension(".json");
259        let result = reg
260            .process(br#"{"secret":"abc"}"#, &profile, &store)
261            .unwrap();
262        assert!(result.is_some());
263    }
264
265    #[test]
266    fn process_returns_none_when_no_processor_matches() {
267        let reg = ProcessorRegistry::new(); // empty
268        let store = make_store();
269        let profile = FileTypeProfile::new("json", vec![]).with_extension(".json");
270        let result = reg.process(b"{}", &profile, &store).unwrap();
271        assert!(result.is_none());
272    }
273
274    #[test]
275    fn default_impl_gives_builtins() {
276        let reg = ProcessorRegistry::default();
277        assert!(reg.get("json").is_some());
278    }
279}