Skip to main content

mime_guess/
lib.rs

1//! Guessing of MIME types by file extension.
2//!
3//! Uses a static list of file-extension : MIME type mappings.
4//!
5//! ```
6//! # extern crate mime;
7//! // the file doesn't have to exist, it just looks at the path
8//! let guess = mime_guess::from_path("some_file.gif");
9//! assert_eq!(guess.first(), Some(mime::IMAGE_GIF));
10//!
11//! ```
12//!
13//! #### Note: MIME Types Returned Are Not Stable/Guaranteed
14//! The media types returned for a given extension are not considered to be part of the crate's
15//! stable API and are often updated in patch <br /> (`x.y.[z + 1]`) releases to be as correct as
16//! possible.
17//!
18//! Additionally, only the extensions of paths/filenames are inspected in order to guess the MIME
19//! type. The file that may or may not reside at that path may or may not be a valid file of the
20//! returned MIME type.  Be wary of unsafe or un-validated assumptions about file structure or
21//! length.
22// pub extern crate mime;
23// extern crate unicase;
24
25use std::{ffi::OsStr, iter, iter::FusedIterator, path::Path, slice};
26
27pub use mime::{self, Mime};
28
29#[cfg(feature = "phf")]
30#[path = "impl_phf.rs"]
31mod impl_;
32
33#[cfg(not(feature = "phf"))]
34#[path = "impl_bin_search.rs"]
35mod impl_;
36
37/// A "guess" of the MIME/Media Type(s) of an extension or path as one or more
38/// [`Mime`](struct.Mime.html) instances.
39///
40/// ### Note: Ordering
41/// A given file format may have one or more applicable Media Types; in this case
42/// the first Media Type returned is whatever is declared in the latest IETF RFC for the
43/// presumed file format or the one that explicitly supercedes all others.
44/// Ordering of additional Media Types is arbitrary.
45///
46/// ### Note: Values Not Stable
47/// The exact Media Types returned in any given guess are not considered to be stable and are often
48/// updated in patch releases in order to reflect the most up-to-date information possible.
49#[derive(Copy, Clone, Debug, PartialEq, Eq)]
50// FIXME: change repr when `mime` gains macro/const fn constructor
51pub struct MimeGuess(&'static [&'static str]);
52
53impl MimeGuess {
54    /// Guess the MIME type of a file (real or otherwise) with the given extension.
55    ///
56    /// The search is case-insensitive.
57    ///
58    /// If `ext` is empty or has no (currently) known MIME type mapping, then an empty guess is
59    /// returned.
60    pub fn from_ext(ext: &str) -> MimeGuess {
61        if ext.is_empty() {
62            return MimeGuess(&[]);
63        }
64
65        impl_::get_mime_types(ext).map_or(MimeGuess(&[]), |v| MimeGuess(v))
66    }
67
68    /// Guess the MIME type of `path` by its extension (as defined by
69    /// [`Path::extension()`]). **No disk access is performed.**
70    ///
71    /// If `path` has no extension, the extension cannot be converted to `str`, or has
72    /// no known MIME type mapping, then an empty guess is returned.
73    ///
74    /// The search is case-insensitive.
75    ///
76    /// ## Note
77    /// **Guess** is the operative word here, as there are no guarantees that the contents of the
78    /// file that `path` points to match the MIME type associated with the path's extension.
79    ///
80    /// Take care when processing files with assumptions based on the return value of this function.
81    ///
82    /// [`Path::extension()`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.extension
83    pub fn from_path<P: AsRef<Path>>(path: P) -> MimeGuess {
84        path.as_ref()
85            .extension()
86            .and_then(OsStr::to_str)
87            .map_or(MimeGuess(&[]), Self::from_ext)
88    }
89
90    /// `true` if the guess did not return any known mappings for the given path or extension.
91    pub fn is_empty(&self) -> bool {
92        self.0.is_empty()
93    }
94
95    /// Get the number of MIME types in the current guess.
96    pub fn count(&self) -> usize {
97        self.0.len()
98    }
99
100    /// Get the first guessed `Mime`, if applicable.
101    ///
102    /// See [Note: Ordering](#note-ordering) above.
103    pub fn first(&self) -> Option<Mime> {
104        self.first_raw().map(expect_mime)
105    }
106
107    /// Get the first guessed Media Type as a string, if applicable.
108    ///
109    /// See [Note: Ordering](#note-ordering) above.
110    pub fn first_raw(&self) -> Option<&'static str> {
111        self.0.get(0).cloned()
112    }
113
114    /// Get the first guessed `Mime`, or if the guess is empty, return
115    /// [`application/octet-stream`] instead.
116    ///
117    /// See [Note: Ordering](#note-ordering) above.
118    ///
119    /// ### Note: HTTP Applications
120    /// For HTTP request and response bodies if a value for the `Content-Type` header
121    /// cannot be determined it might be preferable to not send one at all instead of defaulting to
122    /// `application/octet-stream` as the recipient will expect to infer the format directly from
123    /// the content instead. ([RFC 7231, Section 3.1.1.5][rfc7231])
124    ///
125    /// On the contrary, for `multipart/form-data` bodies, the `Content-Type` of a form-data part is
126    /// assumed to be `text/plain` unless specified so a default of `application/octet-stream`
127    /// for non-text parts is safer. ([RFC 7578, Section 4.4][rfc7578])
128    ///
129    /// [`application/octet-stream`]: https://docs.rs/mime/0.3/mime/constant.APPLICATION_OCTET_STREAM.html
130    /// [rfc7231]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5
131    /// [rfc7578]: https://tools.ietf.org/html/rfc7578#section-4.4
132    pub fn first_or_octet_stream(&self) -> Mime {
133        self.first_or(mime::APPLICATION_OCTET_STREAM)
134    }
135
136    /// Get the first guessed `Mime`, or if the guess is empty, return
137    /// [`text/plain`](::mime::TEXT_PLAIN) instead.
138    ///
139    /// See [Note: Ordering](#note-ordering) above.
140    pub fn first_or_text_plain(&self) -> Mime {
141        self.first_or(mime::TEXT_PLAIN)
142    }
143
144    /// Get the first guessed `Mime`, or if the guess is empty, return the given `Mime` instead.
145    ///
146    /// See [Note: Ordering](#note-ordering) above.
147    pub fn first_or(&self, default: Mime) -> Mime {
148        self.first().unwrap_or(default)
149    }
150
151    /// Get the first guessed `Mime`, or if the guess is empty, execute the closure and return its
152    /// result.
153    ///
154    /// See [Note: Ordering](#note-ordering) above.
155    pub fn first_or_else<F>(&self, default_fn: F) -> Mime
156    where
157        F: FnOnce() -> Mime,
158    {
159        self.first().unwrap_or_else(default_fn)
160    }
161
162    /// Get an iterator over the `Mime` values contained in this guess.
163    ///
164    /// See [Note: Ordering](#note-ordering) above.
165    pub fn iter(&self) -> Iter {
166        Iter(self.iter_raw().map(expect_mime))
167    }
168
169    /// Get an iterator over the raw media-type strings in this guess.
170    ///
171    /// See [Note: Ordering](#note-ordering) above.
172    pub fn iter_raw(&self) -> IterRaw {
173        IterRaw(self.0.iter().cloned())
174    }
175}
176
177impl IntoIterator for MimeGuess {
178    type Item = Mime;
179    type IntoIter = Iter;
180
181    fn into_iter(self) -> Self::IntoIter {
182        self.iter()
183    }
184}
185
186impl<'a> IntoIterator for &'a MimeGuess {
187    type Item = Mime;
188    type IntoIter = Iter;
189
190    fn into_iter(self) -> Self::IntoIter {
191        self.iter()
192    }
193}
194
195/// An iterator over the `Mime` types of a `MimeGuess`.
196///
197/// See [Note: Ordering on `MimeGuess`](struct.MimeGuess.html#note-ordering).
198#[derive(Clone, Debug)]
199pub struct Iter(iter::Map<IterRaw, fn(&'static str) -> Mime>);
200
201impl Iterator for Iter {
202    type Item = Mime;
203
204    fn next(&mut self) -> Option<Self::Item> {
205        self.0.next()
206    }
207
208    fn size_hint(&self) -> (usize, Option<usize>) {
209        self.0.size_hint()
210    }
211}
212
213impl DoubleEndedIterator for Iter {
214    fn next_back(&mut self) -> Option<Self::Item> {
215        self.0.next_back()
216    }
217}
218
219impl FusedIterator for Iter {}
220
221impl ExactSizeIterator for Iter {
222    fn len(&self) -> usize {
223        self.0.len()
224    }
225}
226
227/// An iterator over the raw media type strings of a `MimeGuess`.
228///
229/// See [Note: Ordering on `MimeGuess`](struct.MimeGuess.html#note-ordering).
230#[derive(Clone, Debug)]
231pub struct IterRaw(iter::Cloned<slice::Iter<'static, &'static str>>);
232
233impl Iterator for IterRaw {
234    type Item = &'static str;
235
236    fn next(&mut self) -> Option<Self::Item> {
237        self.0.next()
238    }
239
240    fn size_hint(&self) -> (usize, Option<usize>) {
241        self.0.size_hint()
242    }
243}
244
245impl DoubleEndedIterator for IterRaw {
246    fn next_back(&mut self) -> Option<Self::Item> {
247        self.0.next_back()
248    }
249}
250
251impl FusedIterator for IterRaw {}
252
253impl ExactSizeIterator for IterRaw {
254    fn len(&self) -> usize {
255        self.0.len()
256    }
257}
258
259fn expect_mime(s: &str) -> Mime {
260    // `.parse()` should be checked at compile time to never fail
261    s.parse()
262        .unwrap_or_else(|e| panic!("failed to parse media-type {:?}: {}", s, e))
263}
264
265/// Wrapper of [`MimeGuess::from_ext()`](struct.MimeGuess.html#method.from_ext).
266pub fn from_ext(ext: &str) -> MimeGuess {
267    MimeGuess::from_ext(ext)
268}
269
270/// Wrapper of [`MimeGuess::from_path()`](struct.MimeGuess.html#method.from_path).
271pub fn from_path<P: AsRef<Path>>(path: P) -> MimeGuess {
272    MimeGuess::from_path(path)
273}
274
275/// Guess the MIME type of `path` by its extension (as defined by `Path::extension()`).
276///
277/// If `path` has no extension, or its extension has no known MIME type mapping,
278/// then the MIME type is assumed to be `application/octet-stream`.
279///
280/// ## Note
281/// **Guess** is the operative word here, as there are no guarantees that the contents of the file
282/// that `path` points to match the MIME type associated with the path's extension.
283///
284/// Take care when processing files with assumptions based on the return value of this function.
285///
286/// In HTTP applications, it might be [preferable][rfc7231] to not send a `Content-Type`
287/// header at all instead of defaulting to `application/octet-stream`.
288///
289/// [rfc7231]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5
290#[deprecated(since = "2.0.0", note = "Use `from_path(path).first_or_octet_stream()` instead")]
291pub fn guess_mime_type<P: AsRef<Path>>(path: P) -> Mime {
292    from_path(path).first_or_octet_stream()
293}
294
295/// Guess the MIME type of `path` by its extension (as defined by `Path::extension()`).
296///
297/// If `path` has no extension, or its extension has no known MIME type mapping,
298/// then `None` is returned.
299///
300#[deprecated(since = "2.0.0", note = "Use `from_path(path).first()` instead")]
301pub fn guess_mime_type_opt<P: AsRef<Path>>(path: P) -> Option<Mime> {
302    from_path(path).first()
303}
304
305/// Guess the MIME type string of `path` by its extension (as defined by `Path::extension()`).
306///
307/// If `path` has no extension, or its extension has no known MIME type mapping,
308/// then `None` is returned.
309///
310/// ## Note
311/// **Guess** is the operative word here, as there are no guarantees that the contents of the file
312/// that `path` points to match the MIME type associated with the path's extension.
313///
314/// Take care when processing files with assumptions based on the return value of this function.
315#[deprecated(since = "2.0.0", note = "Use `from_path(path).first_raw()` instead")]
316pub fn mime_str_for_path_ext<P: AsRef<Path>>(path: P) -> Option<&'static str> {
317    from_path(path).first_raw()
318}
319
320/// Get the MIME type associated with a file extension.
321///
322/// If there is no association for the extension, or `ext` is empty,
323/// `application/octet-stream` is returned.
324///
325/// ## Note
326/// In HTTP applications, it might be [preferable][rfc7231] to not send a `Content-Type`
327/// header at all instead of defaulting to `application/octet-stream`.
328///
329/// [rfc7231]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5
330#[deprecated(since = "2.0.0", note = "use `from_ext(search_ext).first_or_octet_stream()` instead")]
331pub fn get_mime_type(search_ext: &str) -> Mime {
332    from_ext(search_ext).first_or_octet_stream()
333}
334
335/// Get the MIME type associated with a file extension.
336///
337/// If there is no association for the extension, or `ext` is empty,
338/// `None` is returned.
339#[deprecated(since = "2.0.0", note = "use `from_ext(search_ext).first()` instead")]
340pub fn get_mime_type_opt(search_ext: &str) -> Option<Mime> {
341    from_ext(search_ext).first()
342}
343
344/// Get the MIME type string associated with a file extension. Case-insensitive.
345///
346/// If `search_ext` is not already lowercase,
347/// it will be converted to lowercase to facilitate the search.
348///
349/// Returns `None` if `search_ext` is empty or an associated extension was not found.
350#[deprecated(since = "2.0.0", note = "use `from_ext(search_ext).first_raw()` instead")]
351pub fn get_mime_type_str(search_ext: &str) -> Option<&'static str> {
352    from_ext(search_ext).first_raw()
353}
354
355/// Get a list of known extensions for a given `Mime`.
356///
357/// Ignores parameters (only searches with `<main type>/<subtype>`). Case-insensitive (for extension types).
358///
359/// Returns `None` if the MIME type is unknown.
360///
361/// ### Wildcards
362/// If the top-level of the MIME type is a wildcard (`*`), returns all extensions.
363///
364/// If the sub-level of the MIME type is a wildcard, returns all extensions for the top-level.
365#[cfg(feature = "rev-mappings")]
366pub fn get_mime_extensions(mime: &Mime) -> Option<&'static [&'static str]> {
367    get_extensions(mime.type_().as_ref(), mime.subtype().as_ref())
368}
369
370/// Get a list of known extensions for a MIME type string.
371///
372/// Ignores parameters (only searches `<main type>/<subtype>`). Case-insensitive.
373///
374/// Returns `None` if the MIME type is unknown.
375///
376/// ### Wildcards
377/// If the top-level of the MIME type is a wildcard (`*`), returns all extensions.
378///
379/// If the sub-level of the MIME type is a wildcard, returns all extensions for the top-level.
380///
381/// ### Panics
382/// If `mime_str` is not a valid MIME type specifier (naive).
383#[cfg(feature = "rev-mappings")]
384pub fn get_mime_extensions_str(mut mime_str: &str) -> Option<&'static [&'static str]> {
385    mime_str = mime_str.trim();
386
387    if let Some(sep_idx) = mime_str.find(';') {
388        mime_str = &mime_str[..sep_idx];
389    }
390
391    let (top, sub) = {
392        let split_idx = mime_str.find('/')?;
393        (&mime_str[..split_idx], &mime_str[split_idx + 1..])
394    };
395
396    get_extensions(top, sub)
397}
398
399/// Get the extensions for a given top-level and sub-level of a MIME type
400/// (`{toplevel}/{sublevel}`).
401///
402/// Returns `None` if `toplevel` or `sublevel` are unknown.
403///
404/// ### Wildcards
405/// If the top-level of the MIME type is a wildcard (`*`), returns all extensions.
406///
407/// If the sub-level of the MIME type is a wildcard, returns all extensions for the top-level.
408#[cfg(feature = "rev-mappings")]
409pub fn get_extensions(toplevel: &str, sublevel: &str) -> Option<&'static [&'static str]> {
410    impl_::get_extensions(toplevel, sublevel)
411}
412
413/// Get the MIME type for `application/octet-stream` (generic binary stream)
414#[deprecated(since = "2.0.0", note = "use `mime::APPLICATION_OCTET_STREAM` instead")]
415pub fn octet_stream() -> Mime {
416    "application/octet-stream".parse().unwrap()
417}
418
419#[cfg(test)]
420mod tests {
421    include!("mime_types.rs");
422
423    #[allow(deprecated, unused_imports)]
424    use std::ascii::AsciiExt;
425    use std::{fmt::Debug, path::Path};
426
427    use super::{expect_mime, from_ext, from_path, get_mime_extensions_str};
428
429    #[test]
430    fn check_type_bounds() {
431        fn assert_type_bounds<T: Clone + Debug + Send + Sync + 'static>() {}
432
433        assert_type_bounds::<super::MimeGuess>();
434        assert_type_bounds::<super::Iter>();
435        assert_type_bounds::<super::IterRaw>();
436    }
437
438    #[test]
439    fn test_mime_type_guessing() {
440        assert_eq!(from_ext("gif").first_or_octet_stream().to_string(), "image/gif".to_string());
441        assert_eq!(from_ext("TXT").first_or_octet_stream().to_string(), "text/plain".to_string());
442        assert_eq!(
443            from_ext("blahblah").first_or_octet_stream().to_string(),
444            "application/octet-stream".to_string()
445        );
446
447        assert_eq!(
448            from_path(Path::new("/path/to/file.gif"))
449                .first_or_octet_stream()
450                .to_string(),
451            "image/gif".to_string()
452        );
453        assert_eq!(
454            from_path("/path/to/file.gif").first_or_octet_stream().to_string(),
455            "image/gif".to_string()
456        );
457    }
458
459    #[test]
460    fn test_mime_type_guessing_opt() {
461        assert_eq!(from_ext("gif").first().unwrap().to_string(), "image/gif".to_string());
462        assert_eq!(from_ext("TXT").first().unwrap().to_string(), "text/plain".to_string());
463        assert_eq!(from_ext("blahblah").first(), None);
464
465        assert_eq!(
466            from_path("/path/to/file.gif").first().unwrap().to_string(),
467            "image/gif".to_string()
468        );
469        assert_eq!(from_path("/path/to/file").first(), None);
470    }
471
472    #[test]
473    fn test_are_mime_types_parseable() {
474        for (_, mimes) in MIME_TYPES {
475            mimes.iter().for_each(|s| {
476                expect_mime(s);
477            });
478        }
479    }
480
481    // RFC: Is this test necessary anymore? --@cybergeek94, 2/1/2016
482    #[test]
483    fn test_are_extensions_ascii() {
484        for (ext, _) in MIME_TYPES {
485            assert!(ext.is_ascii(), "Extension not ASCII: {:?}", ext);
486        }
487    }
488
489    #[test]
490    fn test_are_extensions_sorted() {
491        // simultaneously checks the requirement that duplicate extension entries are adjacent
492        for (&(ext, _), &(n_ext, _)) in MIME_TYPES.iter().zip(MIME_TYPES.iter().skip(1)) {
493            assert!(
494                ext <= n_ext,
495                "Extensions in src/mime_types should be sorted lexicographically
496                in ascending order. Failed assert: {:?} <= {:?}",
497                ext,
498                n_ext
499            );
500        }
501    }
502
503    #[test]
504    fn test_get_mime_extensions_str_no_panic_if_bad_mime() {
505        assert_eq!(get_mime_extensions_str(""), None);
506    }
507}