1use crate::ExifTool;
6use crate::error::{Error, Result};
7use crate::types::TagId;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ChecksumAlgorithm {
13 MD5,
15 SHA1,
17 SHA256,
19 SHA512,
21}
22
23impl ChecksumAlgorithm {
24 pub fn name(&self) -> &'static str {
26 match self {
27 Self::MD5 => "MD5",
28 Self::SHA1 => "SHA1",
29 Self::SHA256 => "SHA256",
30 Self::SHA512 => "SHA512",
31 }
32 }
33
34 pub fn arg(&self) -> String {
36 format!("-{}", self.name())
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct ChecksumResult {
43 pub path: PathBuf,
45 pub checksum: String,
47 pub algorithm: ChecksumAlgorithm,
49}
50
51#[derive(Debug, Clone)]
53pub struct DiffResult {
54 pub is_identical: bool,
56 pub source_only: Vec<String>,
58 pub target_only: Vec<String>,
60 pub different: Vec<(String, String, String)>, }
63
64impl Default for DiffResult {
65 fn default() -> Self {
66 Self {
67 is_identical: true,
68 source_only: Vec::new(),
69 target_only: Vec::new(),
70 different: Vec::new(),
71 }
72 }
73}
74
75impl DiffResult {
76 pub fn new() -> Self {
78 Self::default()
79 }
80
81 pub fn add_source_only(&mut self, tag: impl Into<String>) {
83 self.is_identical = false;
84 self.source_only.push(tag.into());
85 }
86
87 pub fn add_target_only(&mut self, tag: impl Into<String>) {
89 self.is_identical = false;
90 self.target_only.push(tag.into());
91 }
92
93 pub fn add_different(
95 &mut self,
96 tag: impl Into<String>,
97 source: impl Into<String>,
98 target: impl Into<String>,
99 ) {
100 self.is_identical = false;
101 self.different
102 .push((tag.into(), source.into(), target.into()));
103 }
104}
105
106pub trait ConfigOperations {
108 fn with_config<P: AsRef<Path>>(self, config_path: P) -> Self;
110
111 fn calculate_checksum<P: AsRef<Path>>(
113 &self,
114 path: P,
115 algorithm: ChecksumAlgorithm,
116 ) -> Result<ChecksumResult>;
117
118 fn calculate_checksums<P: AsRef<Path>>(
120 &self,
121 path: P,
122 algorithms: &[ChecksumAlgorithm],
123 ) -> Result<Vec<ChecksumResult>>;
124
125 fn verify_checksum<P: AsRef<Path>>(
127 &self,
128 path: P,
129 expected: &str,
130 algorithm: ChecksumAlgorithm,
131 ) -> Result<bool>;
132
133 fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult>;
135
136 fn diff_tags<P: AsRef<Path>, Q: AsRef<Path>>(
138 &self,
139 source: P,
140 target: Q,
141 tags: &[TagId],
142 ) -> Result<DiffResult>;
143}
144
145#[derive(Debug, Clone)]
147pub struct ConfigLoader {
148 config_path: Option<PathBuf>,
149 #[allow(dead_code)]
150 custom_tags: Vec<CustomTag>,
151}
152
153impl Default for ConfigLoader {
154 fn default() -> Self {
155 Self::new()
156 }
157}
158
159impl ConfigLoader {
160 pub fn new() -> Self {
162 Self {
163 config_path: None,
164 custom_tags: Vec::new(),
165 }
166 }
167
168 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self> {
170 let path = path.as_ref();
171 let content = std::fs::read_to_string(path).map_err(Error::Io)?;
172
173 let mut loader = Self::new();
174 loader.config_path = Some(path.to_path_buf());
175 loader.parse_config(&content)?;
176
177 Ok(loader)
178 }
179
180 fn parse_config(&mut self, content: &str) -> Result<()> {
182 for line in content.lines() {
183 let line = line.trim();
184
185 if line.is_empty() || line.starts_with('#') {
187 continue;
188 }
189
190 if line.starts_with("%Image::ExifTool::UserDefined") {
193 self.parse_custom_tag_section(content)?;
194 break;
195 }
196 }
197
198 Ok(())
199 }
200
201 fn parse_custom_tag_section(&mut self, _content: &str) -> Result<()> {
203 Ok(())
206 }
207
208 #[allow(dead_code)]
209 pub fn add_custom_tag(&mut self, tag: CustomTag) {
211 self.custom_tags.push(tag);
212 }
213
214 #[allow(dead_code)]
215 pub fn custom_tags(&self) -> &[CustomTag] {
217 &self.custom_tags
218 }
219
220 #[allow(dead_code)]
221 pub fn config_path(&self) -> Option<&Path> {
223 self.config_path.as_deref()
224 }
225}
226
227#[derive(Debug, Clone)]
229#[allow(dead_code)]
230pub struct CustomTag {
231 pub id: String,
233 pub name: String,
235 pub group: String,
237 pub data_type: TagDataType,
239 pub writable: bool,
241 pub description: Option<String>,
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
247pub enum TagDataType {
248 #[allow(dead_code)]
249 String,
250 #[allow(dead_code)]
251 Integer,
252 #[allow(dead_code)]
253 Rational,
254 #[allow(dead_code)]
255 Binary,
256 #[allow(dead_code)]
257 Undefined,
258}
259
260impl ConfigOperations for ExifTool {
261 fn with_config<P: AsRef<Path>>(self, config_path: P) -> Self {
262 let _ = ConfigLoader::from_file(config_path);
266 self
267 }
268
269 fn calculate_checksum<P: AsRef<Path>>(
270 &self,
271 path: P,
272 algorithm: ChecksumAlgorithm,
273 ) -> Result<ChecksumResult> {
274 let args = vec![
275 algorithm.arg(),
276 "-b".to_string(),
277 path.as_ref().to_string_lossy().to_string(),
278 ];
279
280 let response = self.execute_raw(&args)?;
281 let checksum = response.text().trim().to_string();
282
283 Ok(ChecksumResult {
284 path: path.as_ref().to_path_buf(),
285 checksum,
286 algorithm,
287 })
288 }
289
290 fn calculate_checksums<P: AsRef<Path>>(
291 &self,
292 path: P,
293 algorithms: &[ChecksumAlgorithm],
294 ) -> Result<Vec<ChecksumResult>> {
295 let mut results = Vec::with_capacity(algorithms.len());
296
297 for algo in algorithms {
298 results.push(self.calculate_checksum(path.as_ref(), *algo)?);
299 }
300
301 Ok(results)
302 }
303
304 fn verify_checksum<P: AsRef<Path>>(
305 &self,
306 path: P,
307 expected: &str,
308 algorithm: ChecksumAlgorithm,
309 ) -> Result<bool> {
310 let result = self.calculate_checksum(path, algorithm)?;
311 Ok(result.checksum.eq_ignore_ascii_case(expected))
312 }
313
314 fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult> {
315 let source_meta = self.query(&source).execute()?;
316 let target_meta = self.query(&target).execute()?;
317
318 compare_metadata(&source_meta, &target_meta)
319 }
320
321 fn diff_tags<P: AsRef<Path>, Q: AsRef<Path>>(
322 &self,
323 source: P,
324 target: Q,
325 tags: &[TagId],
326 ) -> Result<DiffResult> {
327 let mut source_query = self.query(&source);
328 let mut target_query = self.query(&target);
329
330 for tag in tags {
331 source_query = source_query.tag_id(*tag);
332 target_query = target_query.tag_id(*tag);
333 }
334
335 let source_meta = source_query.execute()?;
336 let target_meta = target_query.execute()?;
337
338 compare_metadata(&source_meta, &target_meta)
339 }
340}
341
342fn compare_metadata(
344 source: &crate::types::Metadata,
345 target: &crate::types::Metadata,
346) -> Result<DiffResult> {
347 let mut result = DiffResult::new();
348
349 let mut all_tags: std::collections::HashSet<String> = std::collections::HashSet::new();
351 for (tag, _) in source.iter() {
352 all_tags.insert(tag.clone());
353 }
354 for (tag, _) in target.iter() {
355 all_tags.insert(tag.clone());
356 }
357
358 for tag in all_tags {
360 match (source.get(&tag), target.get(&tag)) {
361 (Some(s), Some(t)) => {
362 if s != t {
363 result.add_different(&tag, s.to_string_lossy(), t.to_string_lossy());
364 }
365 }
366 (Some(_), None) => result.add_source_only(&tag),
367 (None, Some(_)) => result.add_target_only(&tag),
368 (None, None) => {} }
370 }
371
372 Ok(result)
373}
374
375#[derive(Debug, Clone, Default)]
377pub struct HexDumpOptions {
378 pub start_offset: Option<usize>,
380 pub length: Option<usize>,
382 pub bytes_per_line: usize,
384}
385
386impl HexDumpOptions {
387 pub fn new() -> Self {
389 Self {
390 start_offset: None,
391 length: None,
392 bytes_per_line: 16,
393 }
394 }
395
396 pub fn start(mut self, offset: usize) -> Self {
398 self.start_offset = Some(offset);
399 self
400 }
401
402 pub fn length(mut self, len: usize) -> Self {
404 self.length = Some(len);
405 self
406 }
407
408 pub fn bytes_per_line(mut self, n: usize) -> Self {
410 self.bytes_per_line = n;
411 self
412 }
413}
414
415pub trait HexDumpOperations {
417 fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String>;
419
420 fn hex_dump_tag<P: AsRef<Path>>(&self, path: P, tag: TagId) -> Result<String>;
422}
423
424impl HexDumpOperations for ExifTool {
425 fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String> {
426 let mut args = vec!["-H".to_string()];
427
428 if let Some(offset) = options.start_offset {
429 args.push(format!("-ge {}", offset));
430 }
431
432 if let Some(length) = options.length {
433 args.push(format!("-le {}", length));
434 }
435
436 args.push(path.as_ref().to_string_lossy().to_string());
437
438 let response = self.execute_raw(&args)?;
439 Ok(response.text())
440 }
441
442 fn hex_dump_tag<P: AsRef<Path>>(&self, path: P, tag: TagId) -> Result<String> {
443 let args = vec![
444 "-H".to_string(),
445 format!("-{}", tag.name()),
446 path.as_ref().to_string_lossy().to_string(),
447 ];
448
449 let response = self.execute_raw(&args)?;
450 Ok(response.text())
451 }
452}
453
454#[derive(Debug, Clone)]
456pub struct VerboseOptions {
457 pub level: u8,
459 pub html_format: bool,
461}
462
463impl VerboseOptions {
464 pub fn new(level: u8) -> Self {
466 Self {
467 level: level.min(5),
468 html_format: false,
469 }
470 }
471
472 pub fn html(mut self) -> Self {
474 self.html_format = true;
475 self
476 }
477
478 pub fn args(&self) -> Vec<String> {
480 let mut args = vec![format!("-v{}", self.level)];
481
482 if self.html_format {
483 args.push("-htmlDump".to_string());
484 }
485
486 args
487 }
488}
489
490pub trait VerboseOperations {
492 fn verbose_dump<P: AsRef<Path>>(&self, path: P, options: &VerboseOptions) -> Result<String>;
494}
495
496impl VerboseOperations for ExifTool {
497 fn verbose_dump<P: AsRef<Path>>(&self, path: P, options: &VerboseOptions) -> Result<String> {
498 let mut args = options.args();
499 args.push(path.as_ref().to_string_lossy().to_string());
500
501 let response = self.execute_raw(&args)?;
502 Ok(response.text())
503 }
504}
505
506#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn test_checksum_algorithm() {
512 assert_eq!(ChecksumAlgorithm::MD5.name(), "MD5");
513 assert_eq!(ChecksumAlgorithm::SHA256.arg(), "-SHA256");
514 }
515
516 #[test]
517 fn test_diff_result() {
518 let mut diff = DiffResult::new();
519 assert!(diff.is_identical);
520
521 diff.add_source_only("Make");
522 assert!(!diff.is_identical);
523 assert_eq!(diff.source_only.len(), 1);
524
525 diff.add_different("Model", "Canon", "Nikon");
526 assert_eq!(diff.different.len(), 1);
527 }
528
529 #[test]
530 fn test_hex_dump_options() {
531 let opts = HexDumpOptions::new()
532 .start(100)
533 .length(256)
534 .bytes_per_line(32);
535
536 assert_eq!(opts.start_offset, Some(100));
537 assert_eq!(opts.length, Some(256));
538 assert_eq!(opts.bytes_per_line, 32);
539 }
540
541 #[test]
542 fn test_verbose_options() {
543 let opts = VerboseOptions::new(3);
544 let args = opts.args();
545 assert!(args.contains(&"-v3".to_string()));
546
547 let opts = VerboseOptions::new(2).html();
548 let args = opts.args();
549 assert!(args.contains(&"-v2".to_string()));
550 assert!(args.contains(&"-htmlDump".to_string()));
551 }
552}