1use std::collections::HashMap;
11
12#[derive(Debug, Clone)]
28pub struct MimeMapper {
29 mappings: HashMap<String, String>,
31}
32
33impl Default for MimeMapper {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl MimeMapper {
40 #[must_use]
51 pub fn new() -> Self {
52 let mut mappings = HashMap::new();
53
54 mappings.insert("elf".to_string(), "application/x-executable".to_string());
56 mappings.insert(
57 "pe32".to_string(),
58 "application/vnd.microsoft.portable-executable".to_string(),
59 );
60 mappings.insert(
61 "pe32+".to_string(),
62 "application/vnd.microsoft.portable-executable".to_string(),
63 );
64 mappings.insert(
65 "mach-o".to_string(),
66 "application/x-mach-binary".to_string(),
67 );
68 mappings.insert("msdos".to_string(), "application/x-dosexec".to_string());
69
70 mappings.insert("zip".to_string(), "application/zip".to_string());
72 mappings.insert("gzip".to_string(), "application/gzip".to_string());
73 mappings.insert("tar".to_string(), "application/x-tar".to_string());
74 mappings.insert("rar".to_string(), "application/vnd.rar".to_string());
75 mappings.insert(
76 "7-zip".to_string(),
77 "application/x-7z-compressed".to_string(),
78 );
79 mappings.insert("bzip2".to_string(), "application/x-bzip2".to_string());
80 mappings.insert("xz".to_string(), "application/x-xz".to_string());
81
82 mappings.insert("jpeg".to_string(), "image/jpeg".to_string());
84 mappings.insert("png".to_string(), "image/png".to_string());
85 mappings.insert("gif".to_string(), "image/gif".to_string());
86 mappings.insert("bmp".to_string(), "image/bmp".to_string());
87 mappings.insert("webp".to_string(), "image/webp".to_string());
88 mappings.insert("tiff".to_string(), "image/tiff".to_string());
89 mappings.insert("ico".to_string(), "image/x-icon".to_string());
90 mappings.insert("svg".to_string(), "image/svg+xml".to_string());
91
92 mappings.insert("pdf".to_string(), "application/pdf".to_string());
94 mappings.insert(
95 "postscript".to_string(),
96 "application/postscript".to_string(),
97 );
98
99 mappings.insert("mp3".to_string(), "audio/mpeg".to_string());
101 mappings.insert("mpeg adts".to_string(), "audio/mpeg".to_string());
102 mappings.insert("mpeg audio".to_string(), "audio/mpeg".to_string());
103 mappings.insert("mp4".to_string(), "video/mp4".to_string());
104 mappings.insert("avi".to_string(), "video/x-msvideo".to_string());
105 mappings.insert("wav".to_string(), "audio/wav".to_string());
106 mappings.insert("ogg".to_string(), "audio/ogg".to_string());
107 mappings.insert("flac".to_string(), "audio/flac".to_string());
108 mappings.insert("webm".to_string(), "video/webm".to_string());
109
110 mappings.insert("html".to_string(), "text/html".to_string());
112 mappings.insert("xml".to_string(), "application/xml".to_string());
113 mappings.insert("json".to_string(), "application/json".to_string());
114 mappings.insert("javascript".to_string(), "text/javascript".to_string());
115 mappings.insert("css".to_string(), "text/css".to_string());
116
117 mappings.insert("ascii".to_string(), "text/plain".to_string());
119 mappings.insert("utf-8".to_string(), "text/plain".to_string());
120 mappings.insert("text".to_string(), "text/plain".to_string());
121
122 mappings.insert(
124 "microsoft word".to_string(),
125 "application/msword".to_string(),
126 );
127 mappings.insert(
128 "microsoft excel".to_string(),
129 "application/vnd.ms-excel".to_string(),
130 );
131 mappings.insert(
132 "microsoft powerpoint".to_string(),
133 "application/vnd.ms-powerpoint".to_string(),
134 );
135
136 Self { mappings }
137 }
138
139 #[must_use]
168 pub fn get_mime_type(&self, description: &str) -> Option<String> {
169 let lower = description.to_lowercase();
170
171 let mut best_match: Option<(&String, &String)> = None;
174
175 for (keyword, mime_type) in &self.mappings {
176 if lower.contains(keyword.as_str()) {
177 match best_match {
178 Some((best_keyword, _)) if keyword.len() > best_keyword.len() => {
179 best_match = Some((keyword, mime_type));
180 }
181 None => {
182 best_match = Some((keyword, mime_type));
183 }
184 _ => {}
185 }
186 }
187 }
188
189 best_match.map(|(_, mime_type)| mime_type.clone())
190 }
191
192 #[must_use]
194 pub fn len(&self) -> usize {
195 self.mappings.len()
196 }
197
198 #[must_use]
200 pub fn is_empty(&self) -> bool {
201 self.mappings.is_empty()
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn test_new_mapper_has_mappings() {
211 let mapper = MimeMapper::new();
212 assert!(!mapper.is_empty());
213 assert!(mapper.len() > 20); }
215
216 #[test]
217 fn test_elf_mime_type() {
218 let mapper = MimeMapper::new();
219 assert_eq!(
220 mapper.get_mime_type("ELF 64-bit LSB executable"),
221 Some("application/x-executable".to_string())
222 );
223 }
224
225 #[test]
226 fn test_pe32_mime_type() {
227 let mapper = MimeMapper::new();
228 assert_eq!(
229 mapper.get_mime_type("PE32 executable"),
230 Some("application/vnd.microsoft.portable-executable".to_string())
231 );
232 }
233
234 #[test]
235 fn test_pe32_plus_mime_type() {
236 let mapper = MimeMapper::new();
237 assert_eq!(
238 mapper.get_mime_type("PE32+ executable (DLL)"),
239 Some("application/vnd.microsoft.portable-executable".to_string())
240 );
241 }
242
243 #[test]
244 fn test_zip_mime_type() {
245 let mapper = MimeMapper::new();
246 assert_eq!(
247 mapper.get_mime_type("Zip archive data"),
248 Some("application/zip".to_string())
249 );
250 }
251
252 #[test]
253 fn test_jpeg_mime_type() {
254 let mapper = MimeMapper::new();
255 assert_eq!(
256 mapper.get_mime_type("JPEG image data, JFIF standard"),
257 Some("image/jpeg".to_string())
258 );
259 }
260
261 #[test]
262 fn test_png_mime_type() {
263 let mapper = MimeMapper::new();
264 assert_eq!(
265 mapper.get_mime_type("PNG image data, 800 x 600"),
266 Some("image/png".to_string())
267 );
268 }
269
270 #[test]
271 fn test_gif_mime_type() {
272 let mapper = MimeMapper::new();
273 assert_eq!(
274 mapper.get_mime_type("GIF image data, version 89a"),
275 Some("image/gif".to_string())
276 );
277 }
278
279 #[test]
280 fn test_pdf_mime_type() {
281 let mapper = MimeMapper::new();
282 assert_eq!(
283 mapper.get_mime_type("PDF document, version 1.4"),
284 Some("application/pdf".to_string())
285 );
286 }
287
288 #[test]
289 fn test_case_insensitive() {
290 let mapper = MimeMapper::new();
291 assert_eq!(
292 mapper.get_mime_type("elf executable"),
293 Some("application/x-executable".to_string())
294 );
295 assert_eq!(
296 mapper.get_mime_type("ELF EXECUTABLE"),
297 Some("application/x-executable".to_string())
298 );
299 }
300
301 #[test]
302 fn test_unknown_type_returns_none() {
303 let mapper = MimeMapper::new();
304 assert_eq!(mapper.get_mime_type("unknown binary format"), None);
305 assert_eq!(mapper.get_mime_type("data"), None);
306 }
307
308 #[test]
309 fn test_gzip_mime_type() {
310 let mapper = MimeMapper::new();
311 assert_eq!(
312 mapper.get_mime_type("gzip compressed data"),
313 Some("application/gzip".to_string())
314 );
315 }
316
317 #[test]
318 fn test_tar_mime_type() {
319 let mapper = MimeMapper::new();
320 assert_eq!(
321 mapper.get_mime_type("POSIX tar archive"),
322 Some("application/x-tar".to_string())
323 );
324 }
325
326 #[test]
327 fn test_html_mime_type() {
328 let mapper = MimeMapper::new();
329 assert_eq!(
330 mapper.get_mime_type("HTML document"),
331 Some("text/html".to_string())
332 );
333 }
334
335 #[test]
336 fn test_json_mime_type() {
337 let mapper = MimeMapper::new();
338 assert_eq!(
339 mapper.get_mime_type("JSON data"),
340 Some("application/json".to_string())
341 );
342 }
343
344 #[test]
345 fn test_mp3_mime_type() {
346 let mapper = MimeMapper::new();
347 assert_eq!(
348 mapper.get_mime_type("Audio file with ID3 version 2.4.0, contains: MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, JntStereo"),
349 Some("audio/mpeg".to_string())
350 );
351 }
352
353 #[test]
354 fn test_default_trait() {
355 let mapper = MimeMapper::default();
356 assert!(!mapper.is_empty());
357 }
358}