exif_oxide/processor_registry/
context.rs1use crate::formats::FileFormat;
8use crate::types::TagValue;
9use std::collections::HashMap;
10
11#[derive(Debug, Clone)]
25pub struct ProcessorContext {
26 pub file_format: FileFormat,
29
30 pub manufacturer: Option<String>,
33
34 pub model: Option<String>,
37
38 pub firmware: Option<String>,
41
42 pub format_version: Option<String>,
45
46 pub table_name: String,
49
50 pub tag_id: Option<u16>,
53
54 pub directory_path: Vec<String>,
57
58 pub data_offset: usize,
61
62 pub parent_tags: HashMap<String, TagValue>,
65
66 pub parameters: HashMap<String, String>,
69
70 pub byte_order: Option<crate::tiff_types::ByteOrder>,
73
74 pub base_offset: usize,
77
78 pub data_size: Option<usize>,
81}
82
83impl ProcessorContext {
84 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 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 pub fn with_manufacturer(mut self, manufacturer: String) -> Self {
122 self.manufacturer = Some(manufacturer);
123 self
124 }
125
126 pub fn with_model(mut self, model: String) -> Self {
128 self.model = Some(model);
129 self
130 }
131
132 pub fn with_firmware(mut self, firmware: String) -> Self {
134 self.firmware = Some(firmware);
135 self
136 }
137
138 pub fn with_format_version(mut self, version: String) -> Self {
140 self.format_version = Some(version);
141 self
142 }
143
144 pub fn with_tag_id(mut self, tag_id: u16) -> Self {
146 self.tag_id = Some(tag_id);
147 self
148 }
149
150 pub fn with_directory_path(mut self, path: Vec<String>) -> Self {
152 self.directory_path = path;
153 self
154 }
155
156 pub fn with_data_offset(mut self, offset: usize) -> Self {
158 self.data_offset = offset;
159 self
160 }
161
162 pub fn with_parent_tags(mut self, tags: HashMap<String, TagValue>) -> Self {
164 self.parent_tags = tags;
165 self
166 }
167
168 pub fn add_parent_tag(&mut self, name: String, value: TagValue) {
170 self.parent_tags.insert(name, value);
171 }
172
173 pub fn with_parent_tag(mut self, name: String, value: TagValue) -> Self {
175 self.parent_tags.insert(name, value);
176 self
177 }
178
179 pub fn with_parameters(mut self, parameters: HashMap<String, String>) -> Self {
181 self.parameters = parameters;
182 self
183 }
184
185 pub fn add_parameter(&mut self, key: String, value: String) {
187 self.parameters.insert(key, value);
188 }
189
190 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 pub fn with_base_offset(mut self, base_offset: usize) -> Self {
198 self.base_offset = base_offset;
199 self
200 }
201
202 pub fn with_data_size(mut self, size: usize) -> Self {
204 self.data_size = Some(size);
205 self
206 }
207
208 pub fn get_parameter(&self, key: &str) -> Option<&String> {
210 self.parameters.get(key)
211 }
212
213 pub fn get_parent_tag(&self, name: &str) -> Option<&TagValue> {
215 self.parent_tags.get(name)
216 }
217
218 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 self.parameters.contains_key(field) || self.parent_tags.contains_key(field)
230 }
231 }
232 }
233
234 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 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 if !self.table_name.is_empty() {
260 derived.directory_path.push(self.table_name.clone());
261 }
262
263 derived
264 }
265
266 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 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 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 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}