//! Guessing of MIME types by file extension. //! //! Uses a static list of file-extension : MIME type mappings. //! //! ``` //! # extern crate mime; //! // the file doesn't have to exist, it just looks at the path //! let guess = mime_guess::from_path("some_file.gif"); //! assert_eq!(guess.first(), Some(mime::IMAGE_GIF)); //! //! ``` //! //! #### Note: MIME Types Returned Are Not Stable/Guaranteed //! The media types returned for a given extension are not considered to be part of the crate's //! stable API and are often updated in patch
(`x.y.[z + 1]`) releases to be as correct as //! possible. //! //! Additionally, only the extensions of paths/filenames are inspected in order to guess the MIME //! type. The file that may or may not reside at that path may or may not be a valid file of the //! returned MIME type. Be wary of unsafe or un-validated assumptions about file structure or //! length. pub extern crate mime; extern crate unicase; pub use mime::Mime; use std::ffi::OsStr; use std::iter::FusedIterator; use std::path::Path; use std::{iter, slice}; #[cfg(feature = "phf")] #[path = "impl_phf.rs"] mod impl_; #[cfg(not(feature = "phf"))] #[path = "impl_bin_search.rs"] mod impl_; /// A "guess" of the MIME/Media Type(s) of an extension or path as one or more /// [`Mime`](struct.Mime.html) instances. /// /// ### Note: Ordering /// A given file format may have one or more applicable Media Types; in this case /// the first Media Type returned is whatever is declared in the latest IETF RFC for the /// presumed file format or the one that explicitly supercedes all others. /// Ordering of additional Media Types is arbitrary. /// /// ### Note: Values Not Stable /// The exact Media Types returned in any given guess are not considered to be stable and are often /// updated in patch releases in order to reflect the most up-to-date information possible. #[derive(Copy, Clone, Debug, PartialEq, Eq)] // FIXME: change repr when `mime` gains macro/const fn constructor pub struct MimeGuess(&'static [&'static str]); impl MimeGuess { /// Guess the MIME type of a file (real or otherwise) with the given extension. /// /// The search is case-insensitive. /// /// If `ext` is empty or has no (currently) known MIME type mapping, then an empty guess is /// returned. pub fn from_ext(ext: &str) -> MimeGuess { if ext.is_empty() { return MimeGuess(&[]); } impl_::get_mime_types(ext).map_or(MimeGuess(&[]), |v| MimeGuess(v)) } /// Guess the MIME type of `path` by its extension (as defined by /// [`Path::extension()`]). **No disk access is performed.** /// /// If `path` has no extension, the extension cannot be converted to `str`, or has /// no known MIME type mapping, then an empty guess is returned. /// /// The search is case-insensitive. /// /// ## Note /// **Guess** is the operative word here, as there are no guarantees that the contents of the /// file that `path` points to match the MIME type associated with the path's extension. /// /// Take care when processing files with assumptions based on the return value of this function. /// /// [`Path::extension()`]: https://doc.rust-lang.org/std/path/struct.Path.html#method.extension pub fn from_path>(path: P) -> MimeGuess { path.as_ref() .extension() .and_then(OsStr::to_str) .map_or(MimeGuess(&[]), Self::from_ext) } /// `true` if the guess did not return any known mappings for the given path or extension. pub fn is_empty(&self) -> bool { self.0.is_empty() } /// Get the number of MIME types in the current guess. pub fn count(&self) -> usize { self.0.len() } /// Get the first guessed `Mime`, if applicable. /// /// See [Note: Ordering](#note-ordering) above. pub fn first(&self) -> Option { self.first_raw().map(expect_mime) } /// Get the first guessed Media Type as a string, if applicable. /// /// See [Note: Ordering](#note-ordering) above. pub fn first_raw(&self) -> Option<&'static str> { self.0.get(0).cloned() } /// Get the first guessed `Mime`, or if the guess is empty, return /// [`application/octet-stream`] instead. /// /// See [Note: Ordering](#note-ordering) above. /// /// ### Note: HTTP Applications /// For HTTP request and response bodies if a value for the `Content-Type` header /// cannot be determined it might be preferable to not send one at all instead of defaulting to /// `application/octet-stream` as the recipient will expect to infer the format directly from /// the content instead. ([RFC 7231, Section 3.1.1.5][rfc7231]) /// /// On the contrary, for `multipart/form-data` bodies, the `Content-Type` of a form-data part is /// assumed to be `text/plain` unless specified so a default of `application/octet-stream` /// for non-text parts is safer. ([RFC 7578, Section 4.4][rfc7578]) /// /// [`application/octet-stream`]: https://docs.rs/mime/0.3/mime/constant.APPLICATION_OCTET_STREAM.html /// [rfc7231]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5 /// [rfc7578]: https://tools.ietf.org/html/rfc7578#section-4.4 pub fn first_or_octet_stream(&self) -> Mime { self.first_or(mime::APPLICATION_OCTET_STREAM) } /// Get the first guessed `Mime`, or if the guess is empty, return /// [`text/plain`](::mime::TEXT_PLAIN) instead. /// /// See [Note: Ordering](#note-ordering) above. pub fn first_or_text_plain(&self) -> Mime { self.first_or(mime::TEXT_PLAIN) } /// Get the first guessed `Mime`, or if the guess is empty, return the given `Mime` instead. /// /// See [Note: Ordering](#note-ordering) above. pub fn first_or(&self, default: Mime) -> Mime { self.first().unwrap_or(default) } /// Get the first guessed `Mime`, or if the guess is empty, execute the closure and return its /// result. /// /// See [Note: Ordering](#note-ordering) above. pub fn first_or_else(&self, default_fn: F) -> Mime where F: FnOnce() -> Mime, { self.first().unwrap_or_else(default_fn) } /// Get an iterator over the `Mime` values contained in this guess. /// /// See [Note: Ordering](#note-ordering) above. pub fn iter(&self) -> Iter { Iter(self.iter_raw().map(expect_mime)) } /// Get an iterator over the raw media-type strings in this guess. /// /// See [Note: Ordering](#note-ordering) above. pub fn iter_raw(&self) -> IterRaw { IterRaw(self.0.iter().cloned()) } } impl IntoIterator for MimeGuess { type Item = Mime; type IntoIter = Iter; fn into_iter(self) -> Self::IntoIter { self.iter() } } impl<'a> IntoIterator for &'a MimeGuess { type Item = Mime; type IntoIter = Iter; fn into_iter(self) -> Self::IntoIter { self.iter() } } /// An iterator over the `Mime` types of a `MimeGuess`. /// /// See [Note: Ordering on `MimeGuess`](struct.MimeGuess.html#note-ordering). #[derive(Clone, Debug)] pub struct Iter(iter::Map Mime>); impl Iterator for Iter { type Item = Mime; fn next(&mut self) -> Option { self.0.next() } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } } impl DoubleEndedIterator for Iter { fn next_back(&mut self) -> Option { self.0.next_back() } } impl FusedIterator for Iter {} impl ExactSizeIterator for Iter { fn len(&self) -> usize { self.0.len() } } /// An iterator over the raw media type strings of a `MimeGuess`. /// /// See [Note: Ordering on `MimeGuess`](struct.MimeGuess.html#note-ordering). #[derive(Clone, Debug)] pub struct IterRaw(iter::Cloned>); impl Iterator for IterRaw { type Item = &'static str; fn next(&mut self) -> Option { self.0.next() } fn size_hint(&self) -> (usize, Option) { self.0.size_hint() } } impl DoubleEndedIterator for IterRaw { fn next_back(&mut self) -> Option { self.0.next_back() } } impl FusedIterator for IterRaw {} impl ExactSizeIterator for IterRaw { fn len(&self) -> usize { self.0.len() } } fn expect_mime(s: &str) -> Mime { // `.parse()` should be checked at compile time to never fail s.parse() .unwrap_or_else(|e| panic!("failed to parse media-type {:?}: {}", s, e)) } /// Wrapper of [`MimeGuess::from_ext()`](struct.MimeGuess.html#method.from_ext). pub fn from_ext(ext: &str) -> MimeGuess { MimeGuess::from_ext(ext) } /// Wrapper of [`MimeGuess::from_path()`](struct.MimeGuess.html#method.from_path). pub fn from_path>(path: P) -> MimeGuess { MimeGuess::from_path(path) } /// Guess the MIME type of `path` by its extension (as defined by `Path::extension()`). /// /// If `path` has no extension, or its extension has no known MIME type mapping, /// then the MIME type is assumed to be `application/octet-stream`. /// /// ## Note /// **Guess** is the operative word here, as there are no guarantees that the contents of the file /// that `path` points to match the MIME type associated with the path's extension. /// /// Take care when processing files with assumptions based on the return value of this function. /// /// In HTTP applications, it might be [preferable][rfc7231] to not send a `Content-Type` /// header at all instead of defaulting to `application/octet-stream`. /// /// [rfc7231]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5 #[deprecated( since = "2.0.0", note = "Use `from_path(path).first_or_octet_stream()` instead" )] pub fn guess_mime_type>(path: P) -> Mime { from_path(path).first_or_octet_stream() } /// Guess the MIME type of `path` by its extension (as defined by `Path::extension()`). /// /// If `path` has no extension, or its extension has no known MIME type mapping, /// then `None` is returned. /// #[deprecated(since = "2.0.0", note = "Use `from_path(path).first()` instead")] pub fn guess_mime_type_opt>(path: P) -> Option { from_path(path).first() } /// Guess the MIME type string of `path` by its extension (as defined by `Path::extension()`). /// /// If `path` has no extension, or its extension has no known MIME type mapping, /// then `None` is returned. /// /// ## Note /// **Guess** is the operative word here, as there are no guarantees that the contents of the file /// that `path` points to match the MIME type associated with the path's extension. /// /// Take care when processing files with assumptions based on the return value of this function. #[deprecated(since = "2.0.0", note = "Use `from_path(path).first_raw()` instead")] pub fn mime_str_for_path_ext>(path: P) -> Option<&'static str> { from_path(path).first_raw() } /// Get the MIME type associated with a file extension. /// /// If there is no association for the extension, or `ext` is empty, /// `application/octet-stream` is returned. /// /// ## Note /// In HTTP applications, it might be [preferable][rfc7231] to not send a `Content-Type` /// header at all instead of defaulting to `application/octet-stream`. /// /// [rfc7231]: https://tools.ietf.org/html/rfc7231#section-3.1.1.5 #[deprecated( since = "2.0.0", note = "use `from_ext(search_ext).first_or_octet_stream()` instead" )] pub fn get_mime_type(search_ext: &str) -> Mime { from_ext(search_ext).first_or_octet_stream() } /// Get the MIME type associated with a file extension. /// /// If there is no association for the extension, or `ext` is empty, /// `None` is returned. #[deprecated(since = "2.0.0", note = "use `from_ext(search_ext).first()` instead")] pub fn get_mime_type_opt(search_ext: &str) -> Option { from_ext(search_ext).first() } /// Get the MIME type string associated with a file extension. Case-insensitive. /// /// If `search_ext` is not already lowercase, /// it will be converted to lowercase to facilitate the search. /// /// Returns `None` if `search_ext` is empty or an associated extension was not found. #[deprecated( since = "2.0.0", note = "use `from_ext(search_ext).first_raw()` instead" )] pub fn get_mime_type_str(search_ext: &str) -> Option<&'static str> { from_ext(search_ext).first_raw() } /// Get a list of known extensions for a given `Mime`. /// /// Ignores parameters (only searches with `
/`). Case-insensitive (for extension types). /// /// Returns `None` if the MIME type is unknown. /// /// ### Wildcards /// If the top-level of the MIME type is a wildcard (`*`), returns all extensions. /// /// If the sub-level of the MIME type is a wildcard, returns all extensions for the top-level. #[cfg(feature = "rev-mappings")] pub fn get_mime_extensions(mime: &Mime) -> Option<&'static [&'static str]> { get_extensions(mime.type_().as_ref(), mime.subtype().as_ref()) } /// Get a list of known extensions for a MIME type string. /// /// Ignores parameters (only searches `
/`). Case-insensitive. /// /// Returns `None` if the MIME type is unknown. /// /// ### Wildcards /// If the top-level of the MIME type is a wildcard (`*`), returns all extensions. /// /// If the sub-level of the MIME type is a wildcard, returns all extensions for the top-level. /// /// ### Panics /// If `mime_str` is not a valid MIME type specifier (naive). #[cfg(feature = "rev-mappings")] pub fn get_mime_extensions_str(mut mime_str: &str) -> Option<&'static [&'static str]> { mime_str = mime_str.trim(); if let Some(sep_idx) = mime_str.find(';') { mime_str = &mime_str[..sep_idx]; } let (top, sub) = { let split_idx = mime_str.find('/')?; (&mime_str[..split_idx], &mime_str[split_idx + 1..]) }; get_extensions(top, sub) } /// Get the extensions for a given top-level and sub-level of a MIME type /// (`{toplevel}/{sublevel}`). /// /// Returns `None` if `toplevel` or `sublevel` are unknown. /// /// ### Wildcards /// If the top-level of the MIME type is a wildcard (`*`), returns all extensions. /// /// If the sub-level of the MIME type is a wildcard, returns all extensions for the top-level. #[cfg(feature = "rev-mappings")] pub fn get_extensions(toplevel: &str, sublevel: &str) -> Option<&'static [&'static str]> { impl_::get_extensions(toplevel, sublevel) } /// Get the MIME type for `application/octet-stream` (generic binary stream) #[deprecated(since = "2.0.0", note = "use `mime::APPLICATION_OCTET_STREAM` instead")] pub fn octet_stream() -> Mime { "application/octet-stream".parse().unwrap() } #[cfg(test)] mod tests { include!("mime_types.rs"); use super::{expect_mime, from_ext, from_path, get_mime_extensions_str}; #[allow(deprecated, unused_imports)] use std::ascii::AsciiExt; use std::fmt::Debug; use std::path::Path; #[test] fn check_type_bounds() { fn assert_type_bounds() {} assert_type_bounds::(); assert_type_bounds::(); assert_type_bounds::(); } #[test] fn test_mime_type_guessing() { assert_eq!( from_ext("gif").first_or_octet_stream().to_string(), "image/gif".to_string() ); assert_eq!( from_ext("TXT").first_or_octet_stream().to_string(), "text/plain".to_string() ); assert_eq!( from_ext("blahblah").first_or_octet_stream().to_string(), "application/octet-stream".to_string() ); assert_eq!( from_path(Path::new("/path/to/file.gif")) .first_or_octet_stream() .to_string(), "image/gif".to_string() ); assert_eq!( from_path("/path/to/file.gif") .first_or_octet_stream() .to_string(), "image/gif".to_string() ); } #[test] fn test_mime_type_guessing_opt() { assert_eq!( from_ext("gif").first().unwrap().to_string(), "image/gif".to_string() ); assert_eq!( from_ext("TXT").first().unwrap().to_string(), "text/plain".to_string() ); assert_eq!(from_ext("blahblah").first(), None); assert_eq!( from_path("/path/to/file.gif").first().unwrap().to_string(), "image/gif".to_string() ); assert_eq!(from_path("/path/to/file").first(), None); } #[test] fn test_are_mime_types_parseable() { for (_, mimes) in MIME_TYPES { mimes.iter().for_each(|s| { expect_mime(s); }); } } // RFC: Is this test necessary anymore? --@cybergeek94, 2/1/2016 #[test] fn test_are_extensions_ascii() { for (ext, _) in MIME_TYPES { assert!(ext.is_ascii(), "Extension not ASCII: {:?}", ext); } } #[test] fn test_are_extensions_sorted() { // simultaneously checks the requirement that duplicate extension entries are adjacent for (&(ext, _), &(n_ext, _)) in MIME_TYPES.iter().zip(MIME_TYPES.iter().skip(1)) { assert!( ext <= n_ext, "Extensions in src/mime_types should be sorted lexicographically in ascending order. Failed assert: {:?} <= {:?}", ext, n_ext ); } } #[test] fn test_get_mime_extensions_str_no_panic_if_bad_mime() { assert_eq!(get_mime_extensions_str(""), None); } }