1use phf::{phf_map, phf_set};
8use std::path::Path;
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum AssetKind {
13 Image,
15 Video,
17}
18
19pub static SUPPORTED: phf::Set<&'static str> = phf_set! {
22 "3fr", "3gp", "3gpp", "ari", "arw", "avi", "avif", "bmp", "cap", "cin",
23 "cr2", "cr3", "crw", "dcr", "dng", "erf", "fff", "flv", "gif", "heic",
24 "heif", "hif", "iiq", "insp", "insv", "jp2", "jpe", "jpeg", "jpg", "jxl",
25 "k25", "kdc", "m2t", "m2ts", "m4v", "mkv", "mov", "mp4", "mpe", "mpeg",
26 "mpg", "mrw", "mts", "mxf", "nef", "nrw", "orf", "ori", "pef",
27 "png", "psd", "raf", "raw", "rw2", "rwl", "sr2", "srf", "srw", "svg",
28 "tif", "tiff", "ts", "vob", "webm", "webp", "wmv", "x3f",
29};
30
31pub static RAW_EXTENSIONS: phf::Set<&'static str> = phf_set! {
34 "3fr", "ari", "arw", "cap", "cin", "cr2", "cr3", "crw", "dcr", "dng",
35 "erf", "fff", "iiq", "k25", "kdc", "mrw", "nef", "nrw", "orf", "ori",
36 "pef", "raf", "raw", "rw2", "rwl", "sr2", "srf", "srw", "x3f",
37};
38
39static MIME_BY_EXT: phf::Map<&'static str, &'static str> = phf_map! {
41 "avif" => "image/avif",
42 "bmp" => "image/bmp",
43 "gif" => "image/gif",
44 "heic" => "image/heic",
45 "heif" => "image/heif",
46 "hif" => "image/heif",
47 "insp" => "image/jpeg",
48 "jpe" => "image/jpeg",
49 "jpeg" => "image/jpeg",
50 "jpg" => "image/jpeg",
51 "jp2" => "image/jp2",
52 "jxl" => "image/jxl",
53 "png" => "image/png",
54 "psd" => "image/vnd.adobe.photoshop",
55 "svg" => "image/svg+xml",
56 "tif" => "image/tiff",
57 "tiff" => "image/tiff",
58 "webp" => "image/webp",
59 "3fr" => "image/x-hasselblad-3fr",
60 "ari" => "image/x-arriflex-ari",
61 "arw" => "image/x-sony-arw",
62 "cap" => "image/x-phaseone-cap",
63 "cin" => "image/cineon",
64 "cr2" => "image/x-canon-cr2",
65 "cr3" => "image/x-canon-cr3",
66 "crw" => "image/x-canon-crw",
67 "dcr" => "image/x-kodak-dcr",
68 "dng" => "image/x-adobe-dng",
69 "erf" => "image/x-epson-erf",
70 "fff" => "image/x-hasselblad-fff",
71 "iiq" => "image/x-phaseone-iiq",
72 "k25" => "image/x-kodak-k25",
73 "kdc" => "image/x-kodak-kdc",
74 "mrw" => "image/x-minolta-mrw",
75 "nef" => "image/x-nikon-nef",
76 "nrw" => "image/x-nikon-nrw",
77 "orf" => "image/x-olympus-orf",
78 "ori" => "image/x-olympus-orf",
79 "pef" => "image/x-pentax-pef",
80 "raf" => "image/x-fuji-raf",
81 "raw" => "image/x-panasonic-raw",
82 "rw2" => "image/x-panasonic-rw2",
83 "rwl" => "image/x-leica-rwl",
84 "sr2" => "image/x-sony-sr2",
85 "srf" => "image/x-sony-sr2",
86 "srw" => "image/x-samsung-srw",
87 "x3f" => "image/x-sigma-x3f",
88 "3gp" => "video/3gpp",
89 "3gpp" => "video/3gpp",
90 "avi" => "video/x-msvideo",
91 "flv" => "video/x-flv",
92 "insv" => "video/mp4",
93 "mp4" => "video/mp4",
94 "m2t" => "video/mp2t",
95 "m2ts" => "video/mp2t",
96 "mts" => "video/mp2t",
97 "ts" => "video/mp2t",
98 "m4v" => "video/x-m4v",
99 "mkv" => "video/x-matroska",
100 "mpe" => "video/mpeg",
101 "mpeg" => "video/mpeg",
102 "mpg" => "video/mpeg",
103 "mov" => "video/quicktime",
104 "mxf" => "application/mxf",
105 "vob" => "video/dvd",
106 "webm" => "video/webm",
107 "wmv" => "video/x-ms-wmv",
108};
109
110pub fn is_supported_path(path: &Path) -> bool {
112 if path.is_dir() {
113 return false;
114 }
115 extension_lower(path)
116 .map(|ext| SUPPORTED.contains(ext.as_str()))
117 .unwrap_or(false)
118}
119
120pub fn is_supported_ext(ext: &str) -> bool {
122 SUPPORTED.contains(ext)
123}
124
125pub fn is_raw_ext(ext: &str) -> bool {
127 RAW_EXTENSIONS.contains(ext)
128}
129
130pub fn is_raw_path(path: &Path) -> bool {
132 extension_lower(path)
133 .map(|ext| RAW_EXTENSIONS.contains(ext.as_str()))
134 .unwrap_or(false)
135}
136
137pub fn mime_for(ext: &str) -> Option<&'static str> {
139 MIME_BY_EXT.get(ext).copied()
140}
141
142pub fn mime_for_path(path: &Path) -> &'static str {
144 extension_lower(path)
145 .and_then(|ext| mime_for(&ext))
146 .unwrap_or("application/octet-stream")
147}
148
149pub fn asset_kind(mime: &str) -> AssetKind {
151 if mime.starts_with("video/") {
152 AssetKind::Video
153 } else {
154 AssetKind::Image
155 }
156}
157
158pub fn is_video_path(path: &Path) -> bool {
160 matches!(
161 extension_lower(path).and_then(|ext| mime_for(&ext)),
162 Some(mime) if mime.starts_with("video/")
163 )
164}
165
166fn extension_lower(path: &Path) -> Option<String> {
168 path.extension()
169 .map(|ext| ext.to_string_lossy().to_lowercase())
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn every_supported_extension_has_a_mime() {
178 for ext in SUPPORTED.iter() {
179 assert!(
180 MIME_BY_EXT.contains_key(ext),
181 "extension `.{}` is supported but has no MIME mapping",
182 ext
183 );
184 }
185 }
186
187 #[test]
188 fn mime_for_path_handles_uppercase_and_unknowns() {
189 assert_eq!(mime_for_path(Path::new("photo.PNG")), "image/png");
190 assert_eq!(mime_for_path(Path::new("photo.jpe")), "image/jpeg");
191 assert_eq!(
192 mime_for_path(Path::new("doc.unknown")),
193 "application/octet-stream"
194 );
195 assert_eq!(
196 mime_for_path(Path::new("noext")),
197 "application/octet-stream"
198 );
199 }
200
201 #[test]
202 fn asset_kind_buckets_video_prefix() {
203 assert_eq!(asset_kind("video/mp4"), AssetKind::Video);
204 assert_eq!(asset_kind("image/jpeg"), AssetKind::Image);
205 }
206
207 #[test]
208 fn is_raw_path_detects_raw_extensions() {
209 assert!(is_raw_path(Path::new("photo.NEF")));
210 assert!(is_raw_path(Path::new("photo.cr3")));
211 assert!(is_raw_path(Path::new("photo.DNG")));
212 assert!(is_raw_path(Path::new("/some/path/IMG.ARW")));
213 assert!(!is_raw_path(Path::new("photo.jpg")));
214 assert!(!is_raw_path(Path::new("video.mp4")));
215 assert!(!is_raw_path(Path::new("noext")));
216 }
217
218 #[test]
219 fn desktop_file_mime_types_match() {
220 use std::collections::BTreeSet;
221 let desktop = include_str!("../setup/dev.nicx.mimick.desktop");
222 let mime_line = desktop
223 .lines()
224 .find(|l| l.starts_with("MimeType="))
225 .expect("No MimeType= line in .desktop file");
226 let declared: BTreeSet<&str> = mime_line
227 .strip_prefix("MimeType=")
228 .unwrap()
229 .split(';')
230 .filter(|s| !s.is_empty())
231 .collect();
232 let source: BTreeSet<&str> = MIME_BY_EXT.values().copied().collect();
233 assert_eq!(
234 declared, source,
235 "MimeType= in .desktop file does not match MIME_BY_EXT in media_kinds.rs"
236 );
237 }
238}