Skip to main content

libmagic_rs/
mime.rs

1// Copyright (c) 2025-2026 the libmagic-rs contributors
2// SPDX-License-Identifier: Apache-2.0
3
4//! MIME type mapping for file type detection
5//!
6//! This module provides MIME type mapping from file type descriptions
7//! to standard MIME types. It includes hardcoded mappings for common
8//! file types.
9
10use std::collections::HashMap;
11
12/// MIME type mapper for converting file descriptions to MIME types
13///
14/// Provides case-insensitive matching of file type descriptions
15/// to their corresponding MIME types.
16///
17/// # Examples
18///
19/// ```
20/// use libmagic_rs::mime::MimeMapper;
21///
22/// let mapper = MimeMapper::new();
23/// assert_eq!(mapper.get_mime_type("ELF 64-bit executable"), Some("application/x-executable".to_string()));
24/// assert_eq!(mapper.get_mime_type("PNG image data"), Some("image/png".to_string()));
25/// assert_eq!(mapper.get_mime_type("unknown format"), None);
26/// ```
27#[derive(Debug, Clone)]
28pub struct MimeMapper {
29    /// Mapping from description keywords to MIME types
30    mappings: HashMap<String, String>,
31}
32
33impl Default for MimeMapper {
34    fn default() -> Self {
35        Self::new()
36    }
37}
38
39impl MimeMapper {
40    /// Create a new MIME mapper with hardcoded mappings
41    ///
42    /// Includes mappings for common file types:
43    /// - Executables (ELF, PE32, Mach-O)
44    /// - Archives (ZIP, GZIP, TAR, RAR, 7Z)
45    /// - Images (JPEG, PNG, GIF, BMP, WEBP, TIFF, ICO)
46    /// - Documents (PDF, PostScript)
47    /// - Audio/Video (MP3, MP4, AVI, WAV)
48    /// - Web (HTML, XML, JSON, JavaScript, CSS)
49    /// - Text formats
50    #[must_use]
51    pub fn new() -> Self {
52        let mut mappings = HashMap::new();
53
54        // Executables
55        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        // Archives
71        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        // Images
83        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        // Documents
93        mappings.insert("pdf".to_string(), "application/pdf".to_string());
94        mappings.insert(
95            "postscript".to_string(),
96            "application/postscript".to_string(),
97        );
98
99        // Audio/Video
100        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        // Web formats
111        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        // Text
118        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        // Office documents
123        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    /// Get MIME type for a file description
140    ///
141    /// Performs case-insensitive matching against known file type keywords.
142    /// Returns the best matching MIME type found in the description,
143    /// preferring longer (more specific) keyword matches.
144    ///
145    /// # Arguments
146    ///
147    /// * `description` - The file type description to match
148    ///
149    /// # Returns
150    ///
151    /// `Some(String)` with the MIME type if a match is found, `None` otherwise.
152    ///
153    /// # Examples
154    ///
155    /// ```
156    /// use libmagic_rs::mime::MimeMapper;
157    ///
158    /// let mapper = MimeMapper::new();
159    ///
160    /// // Case-insensitive matching
161    /// assert_eq!(mapper.get_mime_type("ELF executable"), Some("application/x-executable".to_string()));
162    /// assert_eq!(mapper.get_mime_type("elf executable"), Some("application/x-executable".to_string()));
163    ///
164    /// // Matches within longer descriptions
165    /// assert_eq!(mapper.get_mime_type("PNG image data, 800x600"), Some("image/png".to_string()));
166    /// ```
167    #[must_use]
168    pub fn get_mime_type(&self, description: &str) -> Option<String> {
169        let lower = description.to_lowercase();
170
171        // Find the longest matching keyword for best specificity
172        // e.g., "gzip" should match before "zip" for "gzip compressed"
173        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    /// Get the number of registered MIME mappings
193    #[must_use]
194    pub fn len(&self) -> usize {
195        self.mappings.len()
196    }
197
198    /// Check if the mapper has no mappings
199    #[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); // Should have many mappings
214    }
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}