exif_oxide/processor_registry/
context.rs

1//! ProcessorContext for rich metadata passing between processing layers
2//!
3//! This module provides the ProcessorContext structure that carries comprehensive
4//! metadata about the current processing state, enabling sophisticated processor
5//! selection and conditional dispatch.
6
7use crate::formats::FileFormat;
8use crate::types::TagValue;
9use std::collections::HashMap;
10
11/// Rich context passed to processors for capability assessment and processing
12///
13/// This structure provides all the information a processor needs to determine
14/// if it can handle the current data and to perform sophisticated processing.
15/// It mirrors ExifTool's combination of $self (ExifTool object state) and
16/// $dirInfo (directory information).
17///
18/// ## ExifTool Reference
19///
20/// ExifTool processors receive context through multiple mechanisms:
21/// - `$self` - ExifTool object with extracted tags and state
22/// - `$dirInfo` - Directory information hash with processing parameters
23/// - Global state like current file type, byte order, etc.
24#[derive(Debug, Clone)]
25pub struct ProcessorContext {
26    /// File format being processed
27    /// Used for processor selection (e.g., JPEG vs TIFF vs RAW)
28    pub file_format: FileFormat,
29
30    /// Camera manufacturer extracted from Make tag
31    /// Primary factor in processor selection
32    pub manufacturer: Option<String>,
33
34    /// Camera model extracted from Model tag
35    /// Used for model-specific processor variants
36    pub model: Option<String>,
37
38    /// Firmware version if available
39    /// Some processors need firmware-specific handling
40    pub firmware: Option<String>,
41
42    /// Format version for format-specific processing
43    /// Used by processors that handle multiple format generations
44    pub format_version: Option<String>,
45
46    /// Current table name being processed
47    /// Helps processors understand their context (e.g., "Canon::AFInfo")
48    pub table_name: String,
49
50    /// Current tag ID being processed (if applicable)
51    /// Used for tag-specific processor selection
52    pub tag_id: Option<u16>,
53
54    /// IFD hierarchy path for nested processing
55    /// Tracks the path through nested IFDs (e.g., ["IFD0", "ExifIFD", "MakerNotes"])
56    pub directory_path: Vec<String>,
57
58    /// Current data offset in the file
59    /// Important for offset-based processors and validation
60    pub data_offset: usize,
61
62    /// Previously extracted tags available as context
63    /// Processors can use these for conditional logic and cross-references
64    pub parent_tags: HashMap<String, TagValue>,
65
66    /// Additional parameters from SubDirectory configuration
67    /// ExifTool: SubDirectory parameters like DecryptStart, ByteOrder, etc.
68    pub parameters: HashMap<String, String>,
69
70    /// Byte order for current data processing
71    /// Some processors need explicit byte order information
72    pub byte_order: Option<crate::tiff_types::ByteOrder>,
73
74    /// Base offset for relative address calculations
75    /// Critical for processors that handle offset-based data
76    pub base_offset: usize,
77
78    /// Size of data being processed (if known)
79    /// Used for bounds checking and validation
80    pub data_size: Option<usize>,
81}
82
83impl ProcessorContext {
84    /// Create a new processor context with minimal required information
85    pub fn new(file_format: FileFormat, table_name: String) -> Self {
86        Self {
87            file_format,
88            manufacturer: None,
89            model: None,
90            firmware: None,
91            format_version: None,
92            table_name,
93            tag_id: None,
94            directory_path: Vec::new(),
95            data_offset: 0,
96            parent_tags: HashMap::new(),
97            parameters: HashMap::new(),
98            byte_order: None,
99            base_offset: 0,
100            data_size: None,
101        }
102    }
103
104    /// Create context with manufacturer and model information
105    pub fn with_camera_info(
106        file_format: FileFormat,
107        table_name: String,
108        manufacturer: Option<String>,
109        model: Option<String>,
110    ) -> Self {
111        Self {
112            file_format,
113            manufacturer,
114            model,
115            table_name: table_name.clone(),
116            ..Self::new(file_format, table_name)
117        }
118    }
119
120    /// Set manufacturer information
121    pub fn with_manufacturer(mut self, manufacturer: String) -> Self {
122        self.manufacturer = Some(manufacturer);
123        self
124    }
125
126    /// Set model information
127    pub fn with_model(mut self, model: String) -> Self {
128        self.model = Some(model);
129        self
130    }
131
132    /// Set firmware version
133    pub fn with_firmware(mut self, firmware: String) -> Self {
134        self.firmware = Some(firmware);
135        self
136    }
137
138    /// Set format version
139    pub fn with_format_version(mut self, version: String) -> Self {
140        self.format_version = Some(version);
141        self
142    }
143
144    /// Set current tag ID
145    pub fn with_tag_id(mut self, tag_id: u16) -> Self {
146        self.tag_id = Some(tag_id);
147        self
148    }
149
150    /// Set directory path
151    pub fn with_directory_path(mut self, path: Vec<String>) -> Self {
152        self.directory_path = path;
153        self
154    }
155
156    /// Set data offset
157    pub fn with_data_offset(mut self, offset: usize) -> Self {
158        self.data_offset = offset;
159        self
160    }
161
162    /// Set parent tags
163    pub fn with_parent_tags(mut self, tags: HashMap<String, TagValue>) -> Self {
164        self.parent_tags = tags;
165        self
166    }
167
168    /// Add a parent tag
169    pub fn add_parent_tag(&mut self, name: String, value: TagValue) {
170        self.parent_tags.insert(name, value);
171    }
172
173    /// Add a parent tag (builder pattern)
174    pub fn with_parent_tag(mut self, name: String, value: TagValue) -> Self {
175        self.parent_tags.insert(name, value);
176        self
177    }
178
179    /// Set processing parameters
180    pub fn with_parameters(mut self, parameters: HashMap<String, String>) -> Self {
181        self.parameters = parameters;
182        self
183    }
184
185    /// Add a processing parameter
186    pub fn add_parameter(&mut self, key: String, value: String) {
187        self.parameters.insert(key, value);
188    }
189
190    /// Set byte order
191    pub fn with_byte_order(mut self, byte_order: crate::tiff_types::ByteOrder) -> Self {
192        self.byte_order = Some(byte_order);
193        self
194    }
195
196    /// Set base offset
197    pub fn with_base_offset(mut self, base_offset: usize) -> Self {
198        self.base_offset = base_offset;
199        self
200    }
201
202    /// Set data size
203    pub fn with_data_size(mut self, size: usize) -> Self {
204        self.data_size = Some(size);
205        self
206    }
207
208    /// Get a parameter value by key
209    pub fn get_parameter(&self, key: &str) -> Option<&String> {
210        self.parameters.get(key)
211    }
212
213    /// Get a parent tag value by name
214    pub fn get_parent_tag(&self, name: &str) -> Option<&TagValue> {
215        self.parent_tags.get(name)
216    }
217
218    /// Check if a required context field is available
219    pub fn has_required_field(&self, field: &str) -> bool {
220        match field {
221            "manufacturer" => self.manufacturer.is_some(),
222            "model" => self.model.is_some(),
223            "firmware" => self.firmware.is_some(),
224            "format_version" => self.format_version.is_some(),
225            "tag_id" => self.tag_id.is_some(),
226            "byte_order" => self.byte_order.is_some(),
227            _ => {
228                // Check parameters and parent tags
229                self.parameters.contains_key(field) || self.parent_tags.contains_key(field)
230            }
231        }
232    }
233
234    /// Validate that all required fields are present
235    pub fn validate_required_fields(&self, required_fields: &[String]) -> Result<(), Vec<String>> {
236        let missing_fields: Vec<String> = required_fields
237            .iter()
238            .filter(|field| !self.has_required_field(field))
239            .cloned()
240            .collect();
241
242        if missing_fields.is_empty() {
243            Ok(())
244        } else {
245            Err(missing_fields)
246        }
247    }
248
249    /// Create a derived context for nested processing
250    ///
251    /// This creates a new context based on the current one but updated for
252    /// processing a nested structure (like a SubDirectory).
253    pub fn derive_for_nested(&self, table_name: String, tag_id: Option<u16>) -> Self {
254        let mut derived = self.clone();
255        derived.table_name = table_name;
256        derived.tag_id = tag_id;
257
258        // Add current directory to path for nested processing
259        if !self.table_name.is_empty() {
260            derived.directory_path.push(self.table_name.clone());
261        }
262
263        derived
264    }
265
266    /// Get the current directory path as a string
267    pub fn get_directory_path_string(&self) -> String {
268        if self.directory_path.is_empty() {
269            self.table_name.clone()
270        } else {
271            format!("{}/{}", self.directory_path.join("/"), self.table_name)
272        }
273    }
274
275    /// Check if this context represents a specific manufacturer
276    pub fn is_manufacturer(&self, manufacturer: &str) -> bool {
277        self.manufacturer
278            .as_ref()
279            .map(|m| m.eq_ignore_ascii_case(manufacturer))
280            .unwrap_or(false)
281    }
282
283    /// Check if the model matches a pattern
284    pub fn model_matches(&self, pattern: &str) -> bool {
285        self.model
286            .as_ref()
287            .map(|m| m.contains(pattern))
288            .unwrap_or(false)
289    }
290
291    /// Get encryption key information for Nikon processors
292    ///
293    /// This is a specialized method for Nikon's encrypted data processing
294    /// that extracts the serial number and shutter count for decryption.
295    pub fn get_nikon_encryption_keys(&self) -> Option<(String, u32)> {
296        let serial = self
297            .get_parent_tag("SerialNumber")?
298            .as_string()?
299            .to_string();
300        let shutter_count = self.get_parent_tag("ShutterCount")?.as_u32()?;
301        Some((serial, shutter_count))
302    }
303}
304
305impl Default for ProcessorContext {
306    fn default() -> Self {
307        Self::new(FileFormat::Jpeg, String::new())
308    }
309}
310
311#[cfg(test)]
312mod tests {
313    use super::*;
314
315    #[test]
316    fn test_context_creation() {
317        let context = ProcessorContext::new(FileFormat::Jpeg, "EXIF::Main".to_string());
318        assert_eq!(context.file_format, FileFormat::Jpeg);
319        assert_eq!(context.table_name, "EXIF::Main");
320        assert!(context.manufacturer.is_none());
321    }
322
323    #[test]
324    fn test_context_builder_pattern() {
325        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::Main".to_string())
326            .with_manufacturer("Canon".to_string())
327            .with_model("EOS R5".to_string())
328            .with_tag_id(0x0001);
329
330        assert_eq!(context.manufacturer, Some("Canon".to_string()));
331        assert_eq!(context.model, Some("EOS R5".to_string()));
332        assert_eq!(context.tag_id, Some(0x0001));
333    }
334
335    #[test]
336    fn test_required_field_validation() {
337        let mut context = ProcessorContext::new(FileFormat::Jpeg, "Canon::Main".to_string())
338            .with_manufacturer("Canon".to_string());
339
340        assert!(context.has_required_field("manufacturer"));
341        assert!(!context.has_required_field("model"));
342
343        let required = vec!["manufacturer".to_string(), "model".to_string()];
344        let result = context.validate_required_fields(&required);
345        assert!(result.is_err());
346        assert_eq!(result.unwrap_err(), vec!["model"]);
347
348        context = context.with_model("EOS R5".to_string());
349        let result = context.validate_required_fields(&required);
350        assert!(result.is_ok());
351    }
352
353    #[test]
354    fn test_context_derivation() {
355        let parent_context = ProcessorContext::new(FileFormat::Jpeg, "Canon::Main".to_string())
356            .with_manufacturer("Canon".to_string())
357            .with_directory_path(vec!["IFD0".to_string()]);
358
359        let derived = parent_context.derive_for_nested("Canon::AFInfo".to_string(), Some(0x0001));
360
361        assert_eq!(derived.table_name, "Canon::AFInfo");
362        assert_eq!(derived.tag_id, Some(0x0001));
363        assert_eq!(derived.directory_path, vec!["IFD0", "Canon::Main"]);
364        assert_eq!(derived.manufacturer, Some("Canon".to_string()));
365    }
366
367    #[test]
368    fn test_manufacturer_checking() {
369        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::Main".to_string())
370            .with_manufacturer("CANON".to_string());
371
372        assert!(context.is_manufacturer("Canon"));
373        assert!(context.is_manufacturer("canon"));
374        assert!(context.is_manufacturer("CANON"));
375        assert!(!context.is_manufacturer("Nikon"));
376    }
377
378    #[test]
379    fn test_model_matching() {
380        let context = ProcessorContext::new(FileFormat::Jpeg, "Canon::Main".to_string())
381            .with_model("Canon EOS R5".to_string());
382
383        assert!(context.model_matches("EOS R5"));
384        assert!(context.model_matches("Canon"));
385        assert!(!context.model_matches("R6"));
386    }
387}