Skip to main content

exif_oxide/processor_registry/
dispatch.rs

1//! Dispatch rules for sophisticated processor selection
2//!
3//! This module implements the dispatch rule system that enables sophisticated
4//! processor selection logic beyond simple capability assessment. It captures
5//! ExifTool's conditional dispatch patterns in a structured, extensible way.
6
7use std::sync::Arc;
8use tracing::debug;
9
10use super::{BinaryDataProcessor, ProcessorCapability, ProcessorContext, ProcessorKey};
11
12/// Trait for dispatch rules that influence processor selection
13///
14/// Dispatch rules provide sophisticated logic for processor selection that
15/// goes beyond simple capability assessment. They implement ExifTool's
16/// conditional dispatch patterns found in manufacturer modules.
17///
18/// ## ExifTool Reference
19///
20/// ExifTool uses various conditional dispatch patterns:
21/// ```perl
22/// # Canon.pm conditional dispatch
23/// {
24///     Condition => '$$self{Model} =~ /EOS R5/',
25///     SubDirectory => { ProcessProc => \&ProcessCanonSerialDataMkII }
26/// },
27/// {
28///     Condition => '$$self{Model} =~ /EOS.*Mark/',
29///     SubDirectory => { ProcessProc => \&ProcessCanonSerialData }
30/// }
31/// ```
32///
33/// This trait system captures these patterns in a structured way.
34pub trait DispatchRule: Send + Sync {
35    /// Check if this rule applies to the given context
36    ///
37    /// Returns true if this rule should be considered for processor selection
38    /// in the current context. This allows rules to scope themselves to
39    /// specific manufacturers, file types, or other conditions.
40    fn applies_to(&self, context: &ProcessorContext) -> bool;
41
42    /// Select a processor from the available candidates
43    ///
44    /// Given a list of compatible processors, this rule can select the most
45    /// appropriate one based on its specific logic. Returns None if the rule
46    /// doesn't want to make a selection.
47    ///
48    /// The candidates are provided as (key, processor, capability) tuples.
49    fn select_processor(
50        &self,
51        candidates: &[(
52            ProcessorKey,
53            Arc<dyn BinaryDataProcessor>,
54            ProcessorCapability,
55        )],
56        context: &ProcessorContext,
57    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)>;
58
59    /// Get a human-readable description of this rule
60    fn description(&self) -> &str;
61
62    /// Get the priority of this rule (higher priority rules are evaluated first)
63    fn priority(&self) -> u8 {
64        50 // Default medium priority
65    }
66}
67
68/// Canon-specific dispatch rules
69///
70/// Implements Canon's processor selection logic including model-specific
71/// variants and conditional processor selection.
72///
73/// ## ExifTool Reference
74///
75/// Based on Canon.pm dispatch patterns and conditional processing.
76pub struct CanonDispatchRule;
77
78impl DispatchRule for CanonDispatchRule {
79    fn applies_to(&self, context: &ProcessorContext) -> bool {
80        context.is_manufacturer("Canon")
81    }
82
83    fn select_processor(
84        &self,
85        candidates: &[(
86            ProcessorKey,
87            Arc<dyn BinaryDataProcessor>,
88            ProcessorCapability,
89        )],
90        context: &ProcessorContext,
91    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)> {
92        debug!(
93            "Applying Canon dispatch rule for table: {}",
94            context.table_name
95        );
96
97        // Canon-specific processor selection logic based on ExifTool Canon.pm
98        match context.table_name.as_str() {
99            "Canon::SerialData" => {
100                // Check for newer Canon models that use enhanced serial data format
101                if let Some(model) = &context.model {
102                    if model.contains("EOS R5")
103                        || model.contains("EOS R6")
104                        || model.contains("EOS R3")
105                    {
106                        if let Some(processor) = self.find_processor_variant(
107                            candidates,
108                            "Canon",
109                            "SerialData",
110                            Some("MkII"),
111                        ) {
112                            debug!(
113                                "Selected Canon SerialData MkII processor for model: {}",
114                                model
115                            );
116                            return Some(processor);
117                        }
118                    }
119                }
120
121                // Fall back to standard Canon serial data processor
122                self.find_processor_variant(candidates, "Canon", "SerialData", None)
123            }
124
125            "Canon::AFInfo" | "Canon::AFInfo2" => {
126                // Different AF info processors for different camera generations
127                if let Some(af_info_version) = context.get_parent_tag("AFInfoVersion") {
128                    match af_info_version.as_u16() {
129                        Some(0x0001) => {
130                            self.find_processor_variant(candidates, "Canon", "AFInfo", Some("V1"))
131                        }
132                        Some(0x0002) => {
133                            self.find_processor_variant(candidates, "Canon", "AFInfo", Some("V2"))
134                        }
135                        Some(0x0003) => {
136                            self.find_processor_variant(candidates, "Canon", "AFInfo", Some("V3"))
137                        }
138                        _ => self.find_processor_variant(candidates, "Canon", "AFInfo", None),
139                    }
140                } else {
141                    // Use model-based selection for AF info
142                    if let Some(model) = &context.model {
143                        if model.contains("1D") {
144                            self.find_processor_variant(candidates, "Canon", "AFInfo", Some("1D"))
145                        } else if model.contains("5D") {
146                            self.find_processor_variant(candidates, "Canon", "AFInfo", Some("5D"))
147                        } else {
148                            self.find_processor_variant(candidates, "Canon", "AFInfo", None)
149                        }
150                    } else {
151                        self.find_processor_variant(candidates, "Canon", "AFInfo", None)
152                    }
153                }
154            }
155
156            "Canon::CameraSettings" => {
157                // Camera settings processing - check for format-specific processors
158                if let Some(format_version) = &context.format_version {
159                    self.find_processor_variant(
160                        candidates,
161                        "Canon",
162                        "CameraSettings",
163                        Some(format_version),
164                    )
165                } else {
166                    self.find_processor_variant(candidates, "Canon", "CameraSettings", None)
167                }
168            }
169
170            _ => {
171                // Only handle Canon-specific tables (those starting with "Canon::")
172                // Standard EXIF directories (ExifIFD, GPS, etc.) should use standard processors
173                // ExifTool Exif.pm shows ExifIFD is processed by standard EXIF processor, not Canon-specific
174                if context.table_name.starts_with("Canon::") {
175                    // For other Canon tables, prefer Canon namespace processors
176                    candidates
177                        .iter()
178                        .find(|(key, _, _)| key.namespace == "Canon")
179                        .map(|(key, processor, _)| (key.clone(), processor.clone()))
180                } else {
181                    // Not a Canon-specific table - let standard processor handle it
182                    debug!(
183                        "Canon dispatch rule ignoring non-Canon table: {}",
184                        context.table_name
185                    );
186                    None
187                }
188            }
189        }
190    }
191
192    fn description(&self) -> &str {
193        "Canon manufacturer-specific processor dispatch"
194    }
195
196    fn priority(&self) -> u8 {
197        80 // High priority for manufacturer-specific rules
198    }
199}
200
201impl CanonDispatchRule {
202    /// Find a processor variant by namespace, name, and optional variant
203    fn find_processor_variant(
204        &self,
205        candidates: &[(
206            ProcessorKey,
207            Arc<dyn BinaryDataProcessor>,
208            ProcessorCapability,
209        )],
210        namespace: &str,
211        processor_name: &str,
212        variant: Option<&str>,
213    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)> {
214        candidates
215            .iter()
216            .find(|(key, _, _)| {
217                key.namespace == namespace
218                    && key.processor_name == processor_name
219                    && key.variant.as_deref() == variant
220            })
221            .map(|(key, processor, _)| (key.clone(), processor.clone()))
222    }
223}
224
225/// Nikon-specific dispatch rules
226///
227/// Implements Nikon's processor selection logic including encryption detection
228/// and model-specific processing variants.
229///
230/// ## ExifTool Reference
231///
232/// Based on Nikon.pm dispatch patterns and ProcessNikonEncrypted conditions.
233pub struct NikonDispatchRule;
234
235impl DispatchRule for NikonDispatchRule {
236    fn applies_to(&self, context: &ProcessorContext) -> bool {
237        context.is_manufacturer("NIKON CORPORATION") || context.is_manufacturer("NIKON")
238    }
239
240    fn select_processor(
241        &self,
242        candidates: &[(
243            ProcessorKey,
244            Arc<dyn BinaryDataProcessor>,
245            ProcessorCapability,
246        )],
247        context: &ProcessorContext,
248    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)> {
249        debug!(
250            "Applying Nikon dispatch rule for table: {}",
251            context.table_name
252        );
253
254        // Nikon-specific processor selection logic based on ExifTool Nikon.pm
255        match context.table_name.as_str() {
256            table_name if table_name.contains("LensData") => {
257                // Check for encrypted lens data
258                if context.parameters.contains_key("DecryptStart") {
259                    debug!("Detected encrypted Nikon lens data");
260                    self.find_processor_variant(candidates, "Nikon", "Encrypted", None)
261                } else {
262                    self.find_processor_variant(candidates, "Nikon", "LensData", None)
263                }
264            }
265
266            "Nikon::ShotInfo" => {
267                // Model-specific shot info processors
268                if let Some(model) = &context.model {
269                    if model.contains("Z 9") {
270                        self.find_processor_variant(candidates, "Nikon", "ShotInfo", Some("Z9"))
271                    } else if model.contains("Z 8") {
272                        self.find_processor_variant(candidates, "Nikon", "ShotInfo", Some("Z8"))
273                    } else if model.contains("Z 6III") {
274                        self.find_processor_variant(candidates, "Nikon", "ShotInfo", Some("Z6III"))
275                    } else if model.contains("Z 7II") {
276                        self.find_processor_variant(candidates, "Nikon", "ShotInfo", Some("Z7II"))
277                    } else {
278                        self.find_processor_variant(candidates, "Nikon", "ShotInfo", None)
279                    }
280                } else {
281                    self.find_processor_variant(candidates, "Nikon", "ShotInfo", None)
282                }
283            }
284
285            "Nikon::ColorBalance" => {
286                // Version-specific color balance processors
287                if let Some(version) = context.get_parameter("Version") {
288                    self.find_processor_variant(candidates, "Nikon", "ColorBalance", Some(version))
289                } else {
290                    self.find_processor_variant(candidates, "Nikon", "ColorBalance", None)
291                }
292            }
293
294            _ => {
295                // Only handle Nikon-specific tables (those starting with "Nikon::")
296                // Standard EXIF directories (ExifIFD, GPS, etc.) should use standard processors
297                // ExifTool Nikon.pm shows standard directories are processed by EXIF processor
298                if context.table_name.starts_with("Nikon::") {
299                    // Check if this might be encrypted data based on context
300                    if self.has_encryption_keys(context) && self.might_be_encrypted(context) {
301                        debug!("Trying encrypted processor for: {}", context.table_name);
302                        if let Some(encrypted_processor) =
303                            self.find_processor_variant(candidates, "Nikon", "Encrypted", None)
304                        {
305                            return Some(encrypted_processor);
306                        }
307                    }
308
309                    // Default to any Nikon processor for Nikon-specific tables
310                    candidates
311                        .iter()
312                        .find(|(key, _, _)| key.namespace == "Nikon")
313                        .map(|(key, processor, _)| (key.clone(), processor.clone()))
314                } else {
315                    // Not a Nikon-specific table - let standard processor handle it
316                    debug!(
317                        "Nikon dispatch rule ignoring non-Nikon table: {}",
318                        context.table_name
319                    );
320                    None
321                }
322            }
323        }
324    }
325
326    fn description(&self) -> &str {
327        "Nikon manufacturer-specific processor dispatch with encryption detection"
328    }
329
330    fn priority(&self) -> u8 {
331        80 // High priority for manufacturer-specific rules
332    }
333}
334
335impl NikonDispatchRule {
336    /// Find a processor variant by namespace, name, and optional variant
337    fn find_processor_variant(
338        &self,
339        candidates: &[(
340            ProcessorKey,
341            Arc<dyn BinaryDataProcessor>,
342            ProcessorCapability,
343        )],
344        namespace: &str,
345        processor_name: &str,
346        variant: Option<&str>,
347    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)> {
348        candidates
349            .iter()
350            .find(|(key, _, _)| {
351                key.namespace == namespace
352                    && key.processor_name == processor_name
353                    && key.variant.as_deref() == variant
354            })
355            .map(|(key, processor, _)| (key.clone(), processor.clone()))
356    }
357
358    /// Check if encryption keys are available
359    fn has_encryption_keys(&self, context: &ProcessorContext) -> bool {
360        context.get_nikon_encryption_keys().is_some()
361    }
362
363    /// Check if the current context might contain encrypted data
364    fn might_be_encrypted(&self, context: &ProcessorContext) -> bool {
365        // Check for encryption-related parameters
366        context.parameters.contains_key("DecryptStart")
367            || context.parameters.contains_key("DecryptLen")
368            || context.table_name.contains("Encrypted")
369    }
370}
371
372/// Format-specific dispatch rule
373///
374/// Selects processors based on file format and format-specific requirements.
375/// This rule handles cases where processor selection depends on the file
376/// format rather than manufacturer.
377pub struct FormatDispatchRule;
378
379impl DispatchRule for FormatDispatchRule {
380    fn applies_to(&self, _context: &ProcessorContext) -> bool {
381        true // This rule applies to all contexts
382    }
383
384    fn select_processor(
385        &self,
386        candidates: &[(
387            ProcessorKey,
388            Arc<dyn BinaryDataProcessor>,
389            ProcessorCapability,
390        )],
391        context: &ProcessorContext,
392    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)> {
393        // Format-specific processor selection
394        match context.file_format {
395            crate::formats::FileFormat::Tiff => {
396                // Prefer TIFF-specific processors
397                candidates
398                    .iter()
399                    .find(|(key, _, _)| key.processor_name.contains("TIFF"))
400                    .map(|(key, processor, _)| (key.clone(), processor.clone()))
401            }
402            crate::formats::FileFormat::CanonRaw | crate::formats::FileFormat::NikonRaw => {
403                // Prefer RAW-specific processors
404                candidates
405                    .iter()
406                    .find(|(key, _, _)| key.processor_name.contains("RAW"))
407                    .map(|(key, processor, _)| (key.clone(), processor.clone()))
408            }
409            _ => None, // Let other rules handle
410        }
411    }
412
413    fn description(&self) -> &str {
414        "Format-specific processor dispatch"
415    }
416
417    fn priority(&self) -> u8 {
418        30 // Lower priority than manufacturer rules
419    }
420}
421
422/// Table-specific dispatch rule
423///
424/// Selects processors based on table name patterns and conventions.
425/// This rule implements ExifTool's table-specific processor associations.
426pub struct TableDispatchRule;
427
428impl DispatchRule for TableDispatchRule {
429    fn applies_to(&self, _context: &ProcessorContext) -> bool {
430        true // This rule applies to all contexts
431    }
432
433    fn select_processor(
434        &self,
435        candidates: &[(
436            ProcessorKey,
437            Arc<dyn BinaryDataProcessor>,
438            ProcessorCapability,
439        )],
440        context: &ProcessorContext,
441    ) -> Option<(ProcessorKey, Arc<dyn BinaryDataProcessor>)> {
442        // Table name-based processor selection
443        if context.table_name.contains("BinaryData") {
444            // Prefer binary data processors
445            return candidates
446                .iter()
447                .find(|(key, _, _)| key.processor_name.contains("BinaryData"))
448                .map(|(key, processor, _)| (key.clone(), processor.clone()));
449        }
450
451        if context.table_name.contains("SerialData") {
452            // Prefer serial data processors
453            return candidates
454                .iter()
455                .find(|(key, _, _)| key.processor_name.contains("SerialData"))
456                .map(|(key, processor, _)| (key.clone(), processor.clone()));
457        }
458
459        if context.table_name.contains("AFInfo") {
460            // Prefer AF info processors
461            return candidates
462                .iter()
463                .find(|(key, _, _)| key.processor_name.contains("AFInfo"))
464                .map(|(key, processor, _)| (key.clone(), processor.clone()));
465        }
466
467        None // No specific preference
468    }
469
470    fn description(&self) -> &str {
471        "Table name-based processor dispatch"
472    }
473
474    fn priority(&self) -> u8 {
475        40 // Medium priority
476    }
477}
478
479#[cfg(test)]
480mod tests {
481    use super::*;
482    use crate::formats::FileFormat;
483    use crate::processor_registry::{ProcessorMetadata, ProcessorResult};
484
485    struct MockProcessor;
486    impl BinaryDataProcessor for MockProcessor {
487        fn can_process(&self, _context: &ProcessorContext) -> ProcessorCapability {
488            ProcessorCapability::Good
489        }
490        fn process_data(
491            &self,
492            _data: &[u8],
493            _context: &ProcessorContext,
494        ) -> crate::types::Result<ProcessorResult> {
495            Ok(ProcessorResult::new())
496        }
497        fn get_metadata(&self) -> ProcessorMetadata {
498            ProcessorMetadata::new("Mock".to_string(), "Mock processor".to_string())
499        }
500    }
501
502    #[test]
503    fn test_canon_dispatch_rule() {
504        let rule = CanonDispatchRule;
505        let canon_context =
506            ProcessorContext::new(FileFormat::Jpeg, "Canon::SerialData".to_string())
507                .with_manufacturer("Canon".to_string())
508                .with_model("EOS R5".to_string());
509
510        assert!(rule.applies_to(&canon_context));
511
512        // Test with R5 model - should prefer MkII variant
513        let candidates = vec![
514            (
515                ProcessorKey::new("Canon".to_string(), "SerialData".to_string()),
516                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
517                ProcessorCapability::Good,
518            ),
519            (
520                ProcessorKey::with_variant(
521                    "Canon".to_string(),
522                    "SerialData".to_string(),
523                    "MkII".to_string(),
524                ),
525                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
526                ProcessorCapability::Good,
527            ),
528        ];
529
530        let selected = rule.select_processor(&candidates, &canon_context);
531        assert!(selected.is_some());
532        let (key, _) = selected.unwrap();
533        assert_eq!(key.variant, Some("MkII".to_string()));
534    }
535
536    #[test]
537    fn test_nikon_dispatch_rule() {
538        let rule = NikonDispatchRule;
539        let nikon_context = ProcessorContext::new(FileFormat::Jpeg, "Nikon::LensData".to_string())
540            .with_manufacturer("NIKON CORPORATION".to_string())
541            .with_parameters({
542                let mut params = std::collections::HashMap::new();
543                params.insert("DecryptStart".to_string(), "4".to_string());
544                params
545            });
546
547        assert!(rule.applies_to(&nikon_context));
548
549        let candidates = vec![
550            (
551                ProcessorKey::new("Nikon".to_string(), "LensData".to_string()),
552                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
553                ProcessorCapability::Good,
554            ),
555            (
556                ProcessorKey::new("Nikon".to_string(), "Encrypted".to_string()),
557                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
558                ProcessorCapability::Good,
559            ),
560        ];
561
562        let selected = rule.select_processor(&candidates, &nikon_context);
563        assert!(selected.is_some());
564        let (key, _) = selected.unwrap();
565        assert_eq!(key.processor_name, "Encrypted");
566    }
567
568    #[test]
569    fn test_format_dispatch_rule() {
570        let rule = FormatDispatchRule;
571        let tiff_context = ProcessorContext::new(FileFormat::Tiff, "EXIF::Main".to_string());
572
573        assert!(rule.applies_to(&tiff_context));
574
575        let candidates = vec![
576            (
577                ProcessorKey::new("EXIF".to_string(), "Main".to_string()),
578                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
579                ProcessorCapability::Good,
580            ),
581            (
582                ProcessorKey::new("EXIF".to_string(), "TIFF".to_string()),
583                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
584                ProcessorCapability::Good,
585            ),
586        ];
587
588        let selected = rule.select_processor(&candidates, &tiff_context);
589        assert!(selected.is_some());
590        let (key, _) = selected.unwrap();
591        assert!(key.processor_name.contains("TIFF"));
592    }
593
594    #[test]
595    fn test_table_dispatch_rule() {
596        let rule = TableDispatchRule;
597        let binary_context =
598            ProcessorContext::new(FileFormat::Jpeg, "Canon::BinaryData".to_string());
599
600        assert!(rule.applies_to(&binary_context));
601
602        let candidates = vec![
603            (
604                ProcessorKey::new("Canon".to_string(), "Main".to_string()),
605                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
606                ProcessorCapability::Good,
607            ),
608            (
609                ProcessorKey::new("Canon".to_string(), "BinaryData".to_string()),
610                Arc::new(MockProcessor) as Arc<dyn BinaryDataProcessor>,
611                ProcessorCapability::Good,
612            ),
613        ];
614
615        let selected = rule.select_processor(&candidates, &binary_context);
616        assert!(selected.is_some());
617        let (key, _) = selected.unwrap();
618        assert!(key.processor_name.contains("BinaryData"));
619    }
620}