Skip to main content

exiftool_rs_wrapper/
config.rs

1//! 配置和校验模块
2//!
3//! 支持配置文件加载、自定义标签定义、校验和计算
4
5use crate::ExifTool;
6use crate::error::{Error, Result};
7use crate::types::TagId;
8use std::path::{Path, PathBuf};
9
10/// 校验和算法
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
12pub enum ChecksumAlgorithm {
13    /// MD5
14    MD5,
15    /// SHA1
16    SHA1,
17    /// SHA256
18    SHA256,
19    /// SHA512
20    SHA512,
21}
22
23impl ChecksumAlgorithm {
24    /// 获取算法名称
25    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    /// 获取 ExifTool 参数
35    pub fn arg(&self) -> String {
36        format!("-{}", self.name())
37    }
38}
39
40/// 校验和结果
41#[derive(Debug, Clone)]
42pub struct ChecksumResult {
43    /// 文件路径
44    pub path: PathBuf,
45    /// 校验和值
46    pub checksum: String,
47    /// 算法
48    pub algorithm: ChecksumAlgorithm,
49}
50
51/// 文件比较结果
52#[derive(Debug, Clone)]
53pub struct DiffResult {
54    /// 是否相同
55    pub is_identical: bool,
56    /// 仅在源文件中存在的标签
57    pub source_only: Vec<String>,
58    /// 仅在目标文件中存在的标签
59    pub target_only: Vec<String>,
60    /// 值不同的标签
61    pub different: Vec<(String, String, String)>, // (tag, source_value, target_value)
62}
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    /// 创建新的比较结果
77    pub fn new() -> Self {
78        Self::default()
79    }
80
81    /// 添加仅在源文件存在的标签
82    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    /// 添加仅在目标文件存在的标签
88    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    /// 添加不同的标签
94    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
106/// 配置操作 trait
107pub trait ConfigOperations {
108    /// 加载配置文件
109    fn with_config<P: AsRef<Path>>(self, config_path: P) -> Self;
110
111    /// 计算校验和
112    fn calculate_checksum<P: AsRef<Path>>(
113        &self,
114        path: P,
115        algorithm: ChecksumAlgorithm,
116    ) -> Result<ChecksumResult>;
117
118    /// 计算多个校验和
119    fn calculate_checksums<P: AsRef<Path>>(
120        &self,
121        path: P,
122        algorithms: &[ChecksumAlgorithm],
123    ) -> Result<Vec<ChecksumResult>>;
124
125    /// 验证文件完整性
126    fn verify_checksum<P: AsRef<Path>>(
127        &self,
128        path: P,
129        expected: &str,
130        algorithm: ChecksumAlgorithm,
131    ) -> Result<bool>;
132
133    /// 比较两个文件的元数据
134    fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult>;
135
136    /// 比较两个文件的元数据(仅特定标签)
137    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/// 配置加载器
146#[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    /// 创建新的配置加载器
161    pub fn new() -> Self {
162        Self {
163            config_path: None,
164            custom_tags: Vec::new(),
165        }
166    }
167
168    /// 从文件加载配置
169    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    /// 解析配置内容
181    fn parse_config(&mut self, content: &str) -> Result<()> {
182        for line in content.lines() {
183            let line = line.trim();
184
185            // 跳过空行和注释
186            if line.is_empty() || line.starts_with('#') {
187                continue;
188            }
189
190            // 解析自定义标签定义
191            // 格式: %Image::ExifTool::UserDefined::Main = { ... }
192            if line.starts_with("%Image::ExifTool::UserDefined") {
193                self.parse_custom_tag_section(content)?;
194                break;
195            }
196        }
197
198        Ok(())
199    }
200
201    /// 解析自定义标签段
202    fn parse_custom_tag_section(&mut self, _content: &str) -> Result<()> {
203        // 解析 Perl 配置语法中的自定义标签定义
204        // 这里简化为空实现,完整实现需要解析 Perl 哈希结构
205        Ok(())
206    }
207
208    #[allow(dead_code)]
209    /// 添加自定义标签
210    pub fn add_custom_tag(&mut self, tag: CustomTag) {
211        self.custom_tags.push(tag);
212    }
213
214    #[allow(dead_code)]
215    /// 获取自定义标签
216    pub fn custom_tags(&self) -> &[CustomTag] {
217        &self.custom_tags
218    }
219
220    #[allow(dead_code)]
221    /// 获取配置文件路径
222    pub fn config_path(&self) -> Option<&Path> {
223        self.config_path.as_deref()
224    }
225}
226
227/// 自定义标签定义
228#[derive(Debug, Clone)]
229#[allow(dead_code)]
230pub struct CustomTag {
231    /// 标签 ID(十六进制)
232    pub id: String,
233    /// 标签名称
234    pub name: String,
235    /// 标签组(如 EXIF, IPTC, XMP)
236    pub group: String,
237    /// 数据类型
238    pub data_type: TagDataType,
239    /// 是否可写
240    pub writable: bool,
241    /// 描述信息
242    pub description: Option<String>,
243}
244
245/// 标签数据类型
246#[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        // 保存配置路径,后续在 execute 时使用 -config 参数
263        // 实际实现需要在 ExifTool 结构体中添加 config 字段
264        // 这里暂时返回 self,标记为需要改进
265        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
342/// 比较两个元数据结构
343fn compare_metadata(
344    source: &crate::types::Metadata,
345    target: &crate::types::Metadata,
346) -> Result<DiffResult> {
347    let mut result = DiffResult::new();
348
349    // 收集所有标签
350    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    // 比较每个标签
359    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) => {} // 不可能发生
369        }
370    }
371
372    Ok(result)
373}
374
375/// 十六进制转储选项
376#[derive(Debug, Clone, Default)]
377pub struct HexDumpOptions {
378    /// 起始偏移
379    pub start_offset: Option<usize>,
380    /// 长度限制
381    pub length: Option<usize>,
382    /// 每行字节数
383    pub bytes_per_line: usize,
384}
385
386impl HexDumpOptions {
387    /// 创建新的选项
388    pub fn new() -> Self {
389        Self {
390            start_offset: None,
391            length: None,
392            bytes_per_line: 16,
393        }
394    }
395
396    /// 设置起始偏移
397    pub fn start(mut self, offset: usize) -> Self {
398        self.start_offset = Some(offset);
399        self
400    }
401
402    /// 设置长度
403    pub fn length(mut self, len: usize) -> Self {
404        self.length = Some(len);
405        self
406    }
407
408    /// 设置每行字节数
409    pub fn bytes_per_line(mut self, n: usize) -> Self {
410        self.bytes_per_line = n;
411        self
412    }
413}
414
415/// 十六进制转储 trait
416pub trait HexDumpOperations {
417    /// 获取文件的十六进制转储
418    fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String>;
419
420    /// 获取特定标签的十六进制值
421    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/// 详细输出选项
455#[derive(Debug, Clone)]
456pub struct VerboseOptions {
457    /// 详细级别 (0-5)
458    pub level: u8,
459    /// HTML 格式输出
460    pub html_format: bool,
461}
462
463impl VerboseOptions {
464    /// 创建新的详细输出选项
465    pub fn new(level: u8) -> Self {
466        Self {
467            level: level.min(5),
468            html_format: false,
469        }
470    }
471
472    /// 使用 HTML 格式
473    pub fn html(mut self) -> Self {
474        self.html_format = true;
475        self
476    }
477
478    /// 获取 ExifTool 参数
479    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
490/// 详细输出 trait
491pub trait VerboseOperations {
492    /// 获取详细输出
493    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}