1use std::path::{Path, PathBuf};
11
12pub fn find_sidecar(media_path: &Path) -> Option<PathBuf> {
17 let parent = media_path.parent()?;
18 let filename = media_path.file_name()?.to_str()?;
19
20 let preferred = parent.join(format!("{}.xmp", filename));
22 if case_insensitive_exists(&preferred) {
23 return Some(resolve_case_insensitive(&preferred).unwrap_or(preferred));
24 }
25
26 let stem = media_path.file_stem()?.to_str()?;
28 let fallback = parent.join(format!("{}.xmp", stem));
29 if case_insensitive_exists(&fallback) {
30 return Some(resolve_case_insensitive(&fallback).unwrap_or(fallback));
31 }
32
33 None
34}
35fn case_insensitive_exists(path: &Path) -> bool {
40 if path.exists() {
41 return true;
42 }
43 resolve_case_insensitive(path).is_some()
44}
45
46fn resolve_case_insensitive(path: &Path) -> Option<PathBuf> {
48 let parent = path.parent()?;
49 let target = path.file_name()?.to_str()?.to_ascii_lowercase();
50 let entries = std::fs::read_dir(parent).ok()?;
51 for entry in entries.flatten() {
52 if let Some(name) = entry.file_name().to_str()
53 && name.to_ascii_lowercase() == target
54 {
55 return Some(entry.path());
56 }
57 }
58 None
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64 use std::fs;
65 use tempfile::TempDir;
66
67 fn setup() -> TempDir {
68 tempfile::tempdir().expect("tempdir")
69 }
70
71 #[test]
72 fn preferred_pattern_found() {
73 let dir = setup();
74 let media = dir.path().join("photo.jpg");
75 let sidecar = dir.path().join("photo.jpg.xmp");
76 fs::write(&media, b"img").unwrap();
77 fs::write(&sidecar, b"<xmp/>").unwrap();
78
79 assert_eq!(find_sidecar(&media), Some(sidecar));
80 }
81
82 #[test]
83 fn fallback_pattern_found() {
84 let dir = setup();
85 let media = dir.path().join("photo.cr2");
86 let sidecar = dir.path().join("photo.xmp");
87 fs::write(&media, b"raw").unwrap();
88 fs::write(&sidecar, b"<xmp/>").unwrap();
89
90 assert_eq!(find_sidecar(&media), Some(sidecar));
91 }
92
93 #[test]
94 fn preferred_takes_priority_over_fallback() {
95 let dir = setup();
96 let media = dir.path().join("photo.dng");
97 let preferred = dir.path().join("photo.dng.xmp");
98 let _fallback = dir.path().join("photo.xmp");
99 fs::write(&media, b"raw").unwrap();
100 fs::write(&preferred, b"<preferred/>").unwrap();
101 fs::write(&_fallback, b"<fallback/>").unwrap();
102
103 assert_eq!(find_sidecar(&media), Some(preferred));
104 }
105
106 #[test]
107 fn no_sidecar_returns_none() {
108 let dir = setup();
109 let media = dir.path().join("photo.jpg");
110 fs::write(&media, b"img").unwrap();
111
112 assert_eq!(find_sidecar(&media), None);
113 }
114
115 #[test]
116 fn case_insensitive_extension() {
117 let dir = setup();
118 let media = dir.path().join("photo.nef");
119 let sidecar = dir.path().join("photo.nef.XMP");
120 fs::write(&media, b"raw").unwrap();
121 fs::write(&sidecar, b"<xmp/>").unwrap();
122
123 let result = find_sidecar(&media);
124 assert!(result.is_some(), "should find .XMP variant");
125 assert_eq!(
126 result.unwrap().file_name().unwrap().to_str().unwrap(),
127 "photo.nef.XMP"
128 );
129 }
130}