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