Skip to main content

exiftool_rs_wrapper/
config.rs

1//! 配置和校验模块
2//!
3//! 支持配置文件加载、文件比较、十六进制转储、详细输出
4
5use crate::ExifTool;
6use crate::error::Result;
7use crate::types::TagId;
8use std::path::Path;
9
10/// 文件比较结果
11#[derive(Debug, Clone)]
12pub struct DiffResult {
13    /// 是否相同
14    pub is_identical: bool,
15    /// 仅在源文件中存在的标签
16    pub source_only: Vec<String>,
17    /// 仅在目标文件中存在的标签
18    pub target_only: Vec<String>,
19    /// 值不同的标签
20    pub different: Vec<(String, String, String)>, // (tag, source_value, target_value)
21}
22
23impl Default for DiffResult {
24    fn default() -> Self {
25        Self {
26            is_identical: true,
27            source_only: Vec::new(),
28            target_only: Vec::new(),
29            different: Vec::new(),
30        }
31    }
32}
33
34impl DiffResult {
35    /// 创建新的比较结果
36    pub fn new() -> Self {
37        Self::default()
38    }
39
40    /// 添加仅在源文件存在的标签
41    pub fn add_source_only(&mut self, tag: impl Into<String>) {
42        self.is_identical = false;
43        self.source_only.push(tag.into());
44    }
45
46    /// 添加仅在目标文件存在的标签
47    pub fn add_target_only(&mut self, tag: impl Into<String>) {
48        self.is_identical = false;
49        self.target_only.push(tag.into());
50    }
51
52    /// 添加不同的标签
53    pub fn add_different(
54        &mut self,
55        tag: impl Into<String>,
56        source: impl Into<String>,
57        target: impl Into<String>,
58    ) {
59        self.is_identical = false;
60        self.different
61            .push((tag.into(), source.into(), target.into()));
62    }
63}
64
65/// 配置操作 trait
66pub trait ConfigOperations {
67    /// 加载配置文件(`-config`)
68    fn with_config<P: AsRef<Path>>(&self, config_path: P) -> ExifTool;
69
70    /// 比较两个文件的元数据
71    fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult>;
72
73    /// 比较两个文件的元数据(仅特定标签)
74    fn diff_tags<P: AsRef<Path>, Q: AsRef<Path>>(
75        &self,
76        source: P,
77        target: Q,
78        tags: &[TagId],
79    ) -> Result<DiffResult>;
80}
81
82impl ConfigOperations for ExifTool {
83    fn with_config<P: AsRef<Path>>(&self, config_path: P) -> ExifTool {
84        ExifTool::with_config(self, config_path)
85    }
86
87    fn diff<P: AsRef<Path>, Q: AsRef<Path>>(&self, source: P, target: Q) -> Result<DiffResult> {
88        let source_meta = self.query(&source).execute()?;
89        let target_meta = self.query(&target).execute()?;
90
91        compare_metadata(&source_meta, &target_meta)
92    }
93
94    fn diff_tags<P: AsRef<Path>, Q: AsRef<Path>>(
95        &self,
96        source: P,
97        target: Q,
98        tags: &[TagId],
99    ) -> Result<DiffResult> {
100        let mut source_query = self.query(&source);
101        let mut target_query = self.query(&target);
102
103        for tag in tags {
104            source_query = source_query.tag_id(*tag);
105            target_query = target_query.tag_id(*tag);
106        }
107
108        let source_meta = source_query.execute()?;
109        let target_meta = target_query.execute()?;
110
111        compare_metadata(&source_meta, &target_meta)
112    }
113}
114
115/// 比较两个元数据结构
116fn compare_metadata(
117    source: &crate::types::Metadata,
118    target: &crate::types::Metadata,
119) -> Result<DiffResult> {
120    let mut result = DiffResult::new();
121
122    // 收集所有标签
123    let mut all_tags: std::collections::HashSet<String> = std::collections::HashSet::new();
124    for (tag, _) in source.iter() {
125        all_tags.insert(tag.clone());
126    }
127    for (tag, _) in target.iter() {
128        all_tags.insert(tag.clone());
129    }
130
131    // 比较每个标签
132    for tag in all_tags {
133        match (source.get(&tag), target.get(&tag)) {
134            (Some(s), Some(t)) => {
135                if s != t {
136                    result.add_different(&tag, s.to_string_lossy(), t.to_string_lossy());
137                }
138            }
139            (Some(_), None) => result.add_source_only(&tag),
140            (None, Some(_)) => result.add_target_only(&tag),
141            (None, None) => {} // 不可能发生
142        }
143    }
144
145    Ok(result)
146}
147
148/// 十六进制转储选项
149///
150/// 对应 ExifTool 的 `-htmlDump[OFFSET]` 选项。
151/// ExifTool 原生仅支持起始偏移量,不支持长度限制或每行字节数控制。
152#[derive(Debug, Clone, Default)]
153pub struct HexDumpOptions {
154    /// 起始偏移量(对应 `-htmlDumpOFFSET`)
155    pub start_offset: Option<usize>,
156}
157
158impl HexDumpOptions {
159    /// 创建新的选项
160    pub fn new() -> Self {
161        Self::default()
162    }
163
164    /// 设置起始偏移
165    pub fn start(mut self, offset: usize) -> Self {
166        self.start_offset = Some(offset);
167        self
168    }
169}
170
171/// 十六进制转储 trait
172pub trait HexDumpOperations {
173    /// 获取文件的十六进制转储
174    fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String>;
175
176    /// 获取特定标签的十六进制值
177    fn hex_dump_tag<P: AsRef<Path>>(&self, path: P, tag: TagId) -> Result<String>;
178}
179
180impl HexDumpOperations for ExifTool {
181    fn hex_dump<P: AsRef<Path>>(&self, path: P, options: &HexDumpOptions) -> Result<String> {
182        let mut args = Vec::new();
183
184        if let Some(offset) = options.start_offset {
185            args.push(format!("-htmlDump{}", offset));
186        } else {
187            args.push("-htmlDump".to_string());
188        }
189
190        args.push(path.as_ref().to_string_lossy().to_string());
191
192        let response = self.execute_raw(&args)?;
193        Ok(response.text())
194    }
195
196    fn hex_dump_tag<P: AsRef<Path>>(&self, path: P, tag: TagId) -> Result<String> {
197        let args = vec![
198            "-H".to_string(),
199            format!("-{}", tag.name()),
200            path.as_ref().to_string_lossy().to_string(),
201        ];
202
203        let response = self.execute_raw(&args)?;
204        Ok(response.text())
205    }
206}
207
208/// 详细输出选项
209#[derive(Debug, Clone)]
210pub struct VerboseOptions {
211    /// 详细级别 (0-5)
212    pub level: u8,
213    /// HTML 格式输出
214    pub html_format: bool,
215}
216
217impl VerboseOptions {
218    /// 创建新的详细输出选项
219    pub fn new(level: u8) -> Self {
220        Self {
221            level: level.min(5),
222            html_format: false,
223        }
224    }
225
226    /// 使用 HTML 格式
227    pub fn html(mut self) -> Self {
228        self.html_format = true;
229        self
230    }
231
232    /// 获取 ExifTool 参数
233    pub fn args(&self) -> Vec<String> {
234        let mut args = vec![format!("-v{}", self.level)];
235
236        if self.html_format {
237            args.push("-htmlDump".to_string());
238        }
239
240        args
241    }
242}
243
244/// 详细输出 trait
245pub trait VerboseOperations {
246    /// 获取详细输出
247    fn verbose_dump<P: AsRef<Path>>(&self, path: P, options: &VerboseOptions) -> Result<String>;
248}
249
250impl VerboseOperations for ExifTool {
251    fn verbose_dump<P: AsRef<Path>>(&self, path: P, options: &VerboseOptions) -> Result<String> {
252        let mut args = options.args();
253        args.push(path.as_ref().to_string_lossy().to_string());
254
255        let response = self.execute_raw(&args)?;
256        Ok(response.text())
257    }
258}
259
260#[cfg(test)]
261mod tests {
262    use super::*;
263    use crate::ExifTool;
264    use crate::error::Error;
265
266    /// 最小有效 JPEG 文件字节数组,用于创建临时测试文件
267    const TINY_JPEG: &[u8] = &[
268        0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 0x4A, 0x46, 0x49, 0x46, 0x00, 0x01, 0x01, 0x00, 0x00,
269        0x01, 0x00, 0x01, 0x00, 0x00, 0xFF, 0xDB, 0x00, 0x43, 0x00, 0x08, 0x06, 0x06, 0x07, 0x06,
270        0x05, 0x08, 0x07, 0x07, 0x07, 0x09, 0x09, 0x08, 0x0A, 0x0C, 0x14, 0x0D, 0x0C, 0x0B, 0x0B,
271        0x0C, 0x19, 0x12, 0x13, 0x0F, 0x14, 0x1D, 0x1A, 0x1F, 0x1E, 0x1D, 0x1A, 0x1C, 0x1C, 0x20,
272        0x24, 0x2E, 0x27, 0x20, 0x22, 0x2C, 0x23, 0x1C, 0x1C, 0x28, 0x37, 0x29, 0x2C, 0x30, 0x31,
273        0x34, 0x34, 0x34, 0x1F, 0x27, 0x39, 0x3D, 0x38, 0x32, 0x3C, 0x2E, 0x33, 0x34, 0x32, 0xFF,
274        0xC0, 0x00, 0x0B, 0x08, 0x00, 0x01, 0x00, 0x01, 0x01, 0x01, 0x11, 0x00, 0xFF, 0xC4, 0x00,
275        0x14, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
276        0x00, 0x00, 0x09, 0xFF, 0xC4, 0x00, 0x14, 0x10, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
277        0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFF, 0xDA, 0x00, 0x08, 0x01, 0x01,
278        0x00, 0x00, 0x3F, 0x00, 0xD2, 0xCF, 0x20, 0xFF, 0xD9,
279    ];
280
281    #[test]
282    fn test_diff_result() {
283        let mut diff = DiffResult::new();
284        assert!(diff.is_identical);
285
286        diff.add_source_only("Make");
287        assert!(!diff.is_identical);
288        assert_eq!(diff.source_only.len(), 1);
289
290        diff.add_different("Model", "Canon", "Nikon");
291        assert_eq!(diff.different.len(), 1);
292    }
293
294    #[test]
295    fn test_hex_dump_options() {
296        let opts = HexDumpOptions::new().start(100);
297
298        assert_eq!(opts.start_offset, Some(100));
299    }
300
301    #[test]
302    fn test_verbose_options() {
303        let opts = VerboseOptions::new(3);
304        let args = opts.args();
305        assert!(args.contains(&"-v3".to_string()));
306
307        let opts = VerboseOptions::new(2).html();
308        let args = opts.args();
309        assert!(args.contains(&"-v2".to_string()));
310        assert!(args.contains(&"-htmlDump".to_string()));
311    }
312
313    #[test]
314    fn test_diff_two_different_files() {
315        // 检查 ExifTool 是否可用,不可用则跳过
316        let et = match ExifTool::new() {
317            Ok(et) => et,
318            Err(Error::ExifToolNotFound) => return,
319            Err(e) => panic!("创建 ExifTool 实例时出现意外错误: {:?}", e),
320        };
321
322        // 创建两个内容相同的临时 JPEG 文件
323        let tmp_dir = tempfile::tempdir().expect("创建临时目录失败");
324        let file_a = tmp_dir.path().join("diff_a.jpg");
325        let file_b = tmp_dir.path().join("diff_b.jpg");
326        std::fs::write(&file_a, TINY_JPEG).expect("写入文件 A 失败");
327        std::fs::write(&file_b, TINY_JPEG).expect("写入文件 B 失败");
328
329        // 分别写入不同的元数据,使两个文件产生差异
330        et.write(&file_a)
331            .tag("Artist", "Alice")
332            .tag("Copyright", "2026 Alice")
333            .overwrite_original(true)
334            .execute()
335            .expect("写入文件 A 的元数据失败");
336
337        et.write(&file_b)
338            .tag("Artist", "Bob")
339            .tag("Copyright", "2026 Bob")
340            .overwrite_original(true)
341            .execute()
342            .expect("写入文件 B 的元数据失败");
343
344        // 比较两个文件
345        let diff = et.diff(&file_a, &file_b).expect("执行 diff 操作失败");
346
347        // 验证两个文件不相同
348        assert!(
349            !diff.is_identical,
350            "写入不同元数据后的两个文件的 diff 结果应为不相同"
351        );
352        // 验证差异列表中包含有差异的标签
353        // Artist 和 Copyright 至少会产生差异
354        assert!(
355            !diff.different.is_empty(),
356            "diff 结果的 different 列表不应为空,应至少包含 Artist/Copyright 的差异"
357        );
358    }
359}