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;
11use std::sync::LazyLock;
12
13/// Static, process-wide mapping from lowercased keywords to MIME types.
14///
15/// Built once via [`LazyLock`] rather than per [`MimeMapper`] instance so that
16/// constructing a [`MagicDatabase`](crate::MagicDatabase) does not pay the cost
17/// of 40+ string allocations and [`HashMap`] inserts on every construction.
18static MIME_MAPPINGS: LazyLock<HashMap<&'static str, &'static str>> = LazyLock::new(|| {
19    let entries: &[(&str, &str)] = &[
20        // Executables
21        ("elf", "application/x-executable"),
22        ("pe32", "application/vnd.microsoft.portable-executable"),
23        ("pe32+", "application/vnd.microsoft.portable-executable"),
24        ("mach-o", "application/x-mach-binary"),
25        ("msdos", "application/x-dosexec"),
26        // Archives
27        ("zip", "application/zip"),
28        ("gzip", "application/gzip"),
29        ("tar", "application/x-tar"),
30        ("rar", "application/vnd.rar"),
31        ("7-zip", "application/x-7z-compressed"),
32        ("bzip2", "application/x-bzip2"),
33        ("xz", "application/x-xz"),
34        // Images
35        ("jpeg", "image/jpeg"),
36        ("png", "image/png"),
37        ("gif", "image/gif"),
38        ("bmp", "image/bmp"),
39        ("webp", "image/webp"),
40        ("tiff", "image/tiff"),
41        ("ico", "image/x-icon"),
42        ("svg", "image/svg+xml"),
43        // Documents
44        ("pdf", "application/pdf"),
45        ("postscript", "application/postscript"),
46        // Audio/Video
47        ("mp3", "audio/mpeg"),
48        ("mpeg adts", "audio/mpeg"),
49        ("mpeg audio", "audio/mpeg"),
50        ("mp4", "video/mp4"),
51        ("avi", "video/x-msvideo"),
52        ("wav", "audio/wav"),
53        ("ogg", "audio/ogg"),
54        ("flac", "audio/flac"),
55        ("webm", "video/webm"),
56        // Web formats
57        ("html", "text/html"),
58        ("xml", "application/xml"),
59        ("json", "application/json"),
60        ("javascript", "text/javascript"),
61        ("css", "text/css"),
62        // Text
63        ("ascii", "text/plain"),
64        ("utf-8", "text/plain"),
65        ("text", "text/plain"),
66        // Office documents
67        ("microsoft word", "application/msword"),
68        ("microsoft excel", "application/vnd.ms-excel"),
69        ("microsoft powerpoint", "application/vnd.ms-powerpoint"),
70    ];
71
72    let mut map = HashMap::with_capacity(entries.len());
73    for &(k, v) in entries {
74        map.insert(k, v);
75    }
76    map
77});
78
79/// MIME type mapper for converting file descriptions to MIME types
80///
81/// Provides case-insensitive matching of file type descriptions
82/// to their corresponding MIME types.
83///
84/// Internally backed by a process-wide [`LazyLock`] table, so construction is
85/// effectively free and the underlying mapping is shared across all instances.
86///
87/// # Examples
88///
89/// ```
90/// use libmagic_rs::mime::MimeMapper;
91///
92/// let mapper = MimeMapper::new();
93/// assert_eq!(mapper.get_mime_type("ELF 64-bit executable"), Some("application/x-executable"));
94/// assert_eq!(mapper.get_mime_type("PNG image data"), Some("image/png"));
95/// assert_eq!(mapper.get_mime_type("unknown format"), None);
96/// ```
97#[derive(Debug, Clone, Copy, Default)]
98pub struct MimeMapper {
99    _private: (),
100}
101
102impl MimeMapper {
103    /// Create a new MIME mapper.
104    ///
105    /// This is a zero-cost constructor: all mappings are shared through a
106    /// process-wide [`LazyLock`] table initialized on first use. Repeated calls
107    /// to `new` do not rebuild the mapping.
108    ///
109    /// Includes mappings for common file types:
110    /// - Executables (ELF, PE32, Mach-O)
111    /// - Archives (ZIP, GZIP, TAR, RAR, 7Z)
112    /// - Images (JPEG, PNG, GIF, BMP, WEBP, TIFF, ICO)
113    /// - Documents (PDF, PostScript)
114    /// - Audio/Video (MP3, MP4, AVI, WAV)
115    /// - Web (HTML, XML, JSON, JavaScript, CSS)
116    /// - Text formats
117    #[must_use]
118    pub fn new() -> Self {
119        Self { _private: () }
120    }
121
122    /// Get MIME type for a file description
123    ///
124    /// Performs case-insensitive matching against known file type keywords.
125    /// Returns the best matching MIME type found in the description,
126    /// preferring longer (more specific) keyword matches.
127    ///
128    /// # Arguments
129    ///
130    /// * `description` - The file type description to match
131    ///
132    /// # Returns
133    ///
134    /// `Some(&'static str)` with the MIME type if a match is found, `None` otherwise.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use libmagic_rs::mime::MimeMapper;
140    ///
141    /// let mapper = MimeMapper::new();
142    ///
143    /// // Case-insensitive matching
144    /// assert_eq!(mapper.get_mime_type("ELF executable"), Some("application/x-executable"));
145    /// assert_eq!(mapper.get_mime_type("elf executable"), Some("application/x-executable"));
146    ///
147    /// // Matches within longer descriptions
148    /// assert_eq!(mapper.get_mime_type("PNG image data, 800x600"), Some("image/png"));
149    /// ```
150    #[must_use]
151    pub fn get_mime_type(&self, description: &str) -> Option<&'static str> {
152        let lower = description.to_lowercase();
153
154        // Find the longest matching keyword for best specificity
155        // e.g., "gzip" should match before "zip" for "gzip compressed"
156        let mut best_match: Option<(&'static str, &'static str)> = None;
157
158        for (keyword, mime_type) in MIME_MAPPINGS.iter() {
159            if lower.contains(*keyword) {
160                match best_match {
161                    Some((best_keyword, _)) if keyword.len() > best_keyword.len() => {
162                        best_match = Some((*keyword, *mime_type));
163                    }
164                    None => {
165                        best_match = Some((*keyword, *mime_type));
166                    }
167                    _ => {}
168                }
169            }
170        }
171
172        best_match.map(|(_, mime_type)| mime_type)
173    }
174
175    /// Get the number of registered MIME mappings
176    #[must_use]
177    pub fn len(&self) -> usize {
178        MIME_MAPPINGS.len()
179    }
180
181    /// Check if the mapper has no mappings
182    #[must_use]
183    pub fn is_empty(&self) -> bool {
184        MIME_MAPPINGS.is_empty()
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    #[test]
193    fn test_new_mapper_has_mappings() {
194        let mapper = MimeMapper::new();
195        assert!(!mapper.is_empty());
196        assert!(mapper.len() > 20); // Should have many mappings
197    }
198
199    #[test]
200    fn test_elf_mime_type() {
201        let mapper = MimeMapper::new();
202        assert_eq!(
203            mapper.get_mime_type("ELF 64-bit LSB executable"),
204            Some("application/x-executable")
205        );
206    }
207
208    #[test]
209    fn test_pe32_mime_type() {
210        let mapper = MimeMapper::new();
211        assert_eq!(
212            mapper.get_mime_type("PE32 executable"),
213            Some("application/vnd.microsoft.portable-executable")
214        );
215    }
216
217    #[test]
218    fn test_pe32_plus_mime_type() {
219        let mapper = MimeMapper::new();
220        assert_eq!(
221            mapper.get_mime_type("PE32+ executable (DLL)"),
222            Some("application/vnd.microsoft.portable-executable")
223        );
224    }
225
226    #[test]
227    fn test_zip_mime_type() {
228        let mapper = MimeMapper::new();
229        assert_eq!(
230            mapper.get_mime_type("Zip archive data"),
231            Some("application/zip")
232        );
233    }
234
235    #[test]
236    fn test_jpeg_mime_type() {
237        let mapper = MimeMapper::new();
238        assert_eq!(
239            mapper.get_mime_type("JPEG image data, JFIF standard"),
240            Some("image/jpeg")
241        );
242    }
243
244    #[test]
245    fn test_png_mime_type() {
246        let mapper = MimeMapper::new();
247        assert_eq!(
248            mapper.get_mime_type("PNG image data, 800 x 600"),
249            Some("image/png")
250        );
251    }
252
253    #[test]
254    fn test_gif_mime_type() {
255        let mapper = MimeMapper::new();
256        assert_eq!(
257            mapper.get_mime_type("GIF image data, version 89a"),
258            Some("image/gif")
259        );
260    }
261
262    #[test]
263    fn test_pdf_mime_type() {
264        let mapper = MimeMapper::new();
265        assert_eq!(
266            mapper.get_mime_type("PDF document, version 1.4"),
267            Some("application/pdf")
268        );
269    }
270
271    #[test]
272    fn test_case_insensitive() {
273        let mapper = MimeMapper::new();
274        assert_eq!(
275            mapper.get_mime_type("elf executable"),
276            Some("application/x-executable")
277        );
278        assert_eq!(
279            mapper.get_mime_type("ELF EXECUTABLE"),
280            Some("application/x-executable")
281        );
282    }
283
284    #[test]
285    fn test_unknown_type_returns_none() {
286        let mapper = MimeMapper::new();
287        assert_eq!(mapper.get_mime_type("unknown binary format"), None);
288        assert_eq!(mapper.get_mime_type("data"), None);
289    }
290
291    #[test]
292    fn test_gzip_mime_type() {
293        let mapper = MimeMapper::new();
294        assert_eq!(
295            mapper.get_mime_type("gzip compressed data"),
296            Some("application/gzip")
297        );
298    }
299
300    #[test]
301    fn test_tar_mime_type() {
302        let mapper = MimeMapper::new();
303        assert_eq!(
304            mapper.get_mime_type("POSIX tar archive"),
305            Some("application/x-tar")
306        );
307    }
308
309    #[test]
310    fn test_html_mime_type() {
311        let mapper = MimeMapper::new();
312        assert_eq!(mapper.get_mime_type("HTML document"), Some("text/html"));
313    }
314
315    #[test]
316    fn test_json_mime_type() {
317        let mapper = MimeMapper::new();
318        assert_eq!(mapper.get_mime_type("JSON data"), Some("application/json"));
319    }
320
321    #[test]
322    fn test_mp3_mime_type() {
323        let mapper = MimeMapper::new();
324        assert_eq!(
325            mapper.get_mime_type("Audio file with ID3 version 2.4.0, contains: MPEG ADTS, layer III, v1, 128 kbps, 44.1 kHz, JntStereo"),
326            Some("audio/mpeg")
327        );
328    }
329
330    #[test]
331    fn test_default_trait() {
332        let mapper = MimeMapper::default();
333        assert!(!mapper.is_empty());
334    }
335}