| //! Guessing of MIME types by file extension. | |
| //! | |
| //! Uses a static list of file-extension : MIME type mappings. | |
| //! | |
| //! #### 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) 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. | |
| #![cfg_attr(feature = "bench", feature(test))] | |
| extern crate mime; | |
| extern crate phf; | |
| extern crate unicase; | |
| pub use mime::Mime; | |
| use unicase::UniCase; | |
| use std::ffi::OsStr; | |
| use std::path::Path; | |
| include!(concat!(env!("OUT_DIR"), "/mime_types_generated.rs")); | |
| struct TopLevelExts { | |
| start: usize, | |
| end: usize, | |
| subs: phf::Map<UniCase<&'static str>, (usize, usize)>, | |
| } | |
| macro_rules! try_opt ( | |
| ($expr:expr) => ( | |
| match $expr { | |
| Some(val) => val, | |
| None => return None, | |
| } | |
| ) | |
| ); | |
| #[cfg(test)] | |
| #[path = "mime_types.rs"] | |
| mod mime_types_src; | |
| /// 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. | |
| pub fn guess_mime_type<P: AsRef<Path>>(path: P) -> Mime { | |
| guess_mime_type_opt(path) | |
| .unwrap_or_else(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. | |
| /// | |
| /// ## 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. | |
| pub fn guess_mime_type_opt<P: AsRef<Path>>(path: P) -> Option<Mime> { | |
| let ext = path.as_ref().extension().and_then(OsStr::to_str).unwrap_or(""); | |
| get_mime_type_opt(ext) | |
| } | |
| /// 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. | |
| pub fn get_mime_type(search_ext: &str) -> Mime { | |
| get_mime_type_opt(search_ext) | |
| .unwrap_or_else(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. | |
| pub fn get_mime_type_opt(search_ext: &str) -> Option<Mime> { | |
| get_mime_type_str(search_ext) | |
| .map(|mime| mime.parse::<Mime>().unwrap()) | |
| } | |
| /// 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. | |
| pub fn get_mime_type_str(search_ext: &str) -> Option<&'static str> { | |
| if search_ext.is_empty() { return None; } | |
| map_lookup(&MIME_TYPES, search_ext).cloned() | |
| } | |
| /// Get a list of known extensions for a given `Mime`. | |
| /// | |
| /// Ignores parameters (only searches with `<main type>/<subtype>`). 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. | |
| pub fn get_mime_extensions(mime: &Mime) -> Option<&'static [&'static str]> { | |
| get_extensions(&mime.0, &mime.1) | |
| } | |
| /// Get a list of known extensions for a MIME type string. | |
| /// | |
| /// Ignores parameters (only searches `<main type>/<subtype>`). 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). | |
| 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('/').unwrap(); | |
| (&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. | |
| pub fn get_extensions(toplevel: &str, sublevel: &str) -> Option<&'static [&'static str]> { | |
| if toplevel == "*" { | |
| return Some(EXTS); | |
| } | |
| let top = try_opt!(map_lookup(&REV_MAPPINGS, toplevel)); | |
| if sublevel == "*" { | |
| return Some(&EXTS[top.start .. top.end]); | |
| } | |
| let sub = try_opt!(map_lookup(&top.subs, sublevel)); | |
| Some(&EXTS[sub.0 .. sub.1]) | |
| } | |
| /// Get the MIME type for `application/octet-stream` (generic binary stream) | |
| pub fn octet_stream() -> Mime { | |
| "application/octet-stream".parse().unwrap() | |
| } | |
| fn map_lookup<'map, V>(map: &'map phf::Map<UniCase<&'static str>, V>, key: &str) -> Option<&'map V> { | |
| // This transmute should be safe as `get` will not store the reference with | |
| // the expanded lifetime. This is due to `Borrow` being overly strict and | |
| // can't have an impl for `&'static str` to `Borrow<&'a str>`. | |
| // | |
| // See https://github.com/rust-lang/rust/issues/28853#issuecomment-158735548 | |
| let key = unsafe { ::std::mem::transmute::<_, &'static str>(key) }; | |
| map.get(&UniCase(key)) | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use mime::Mime; | |
| use std::ascii::AsciiExt; | |
| use std::path::Path; | |
| use super::{get_mime_type, guess_mime_type, MIME_TYPES}; | |
| use super::{get_mime_type_opt, guess_mime_type_opt}; | |
| #[test] | |
| fn test_mime_type_guessing() { | |
| assert_eq!(get_mime_type("gif").to_string(), "image/gif".to_string()); | |
| assert_eq!(get_mime_type("TXT").to_string(), "text/plain".to_string()); | |
| assert_eq!(get_mime_type("blahblah").to_string(), "application/octet-stream".to_string()); | |
| assert_eq!(guess_mime_type(Path::new("/path/to/file.gif")).to_string(), "image/gif".to_string()); | |
| assert_eq!(guess_mime_type("/path/to/file.gif").to_string(), "image/gif".to_string()); | |
| } | |
| #[test] | |
| fn test_mime_type_guessing_opt() { | |
| assert_eq!(get_mime_type_opt("gif").unwrap().to_string(), "image/gif".to_string()); | |
| assert_eq!(get_mime_type_opt("TXT").unwrap().to_string(), "text/plain".to_string()); | |
| assert_eq!(get_mime_type_opt("blahblah"), None); | |
| assert_eq!(guess_mime_type_opt("/path/to/file.gif").unwrap().to_string(), "image/gif".to_string()); | |
| assert_eq!(guess_mime_type_opt("/path/to/file"), None); | |
| } | |
| #[test] | |
| fn test_are_mime_types_parseable() { | |
| for (_, mime) in &MIME_TYPES { | |
| mime.parse::<Mime>().unwrap(); | |
| } | |
| } | |
| // 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() { | |
| use mime_types_src::MIME_TYPES; | |
| 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 alphabetically | |
| in ascending order. Failed assert: {:?} <= {:?}", | |
| ext, n_ext | |
| ); | |
| } | |
| } | |
| } | |
| #[cfg(feature = "bench")] | |
| mod bench { | |
| extern crate test; | |
| use self::test::Bencher; | |
| use super::{get_mime_type_str, MIME_TYPES}; | |
| /// WARNING: this may take a while! | |
| #[bench] | |
| fn bench_mime_str(b: &mut Bencher) { | |
| for (mime_ext, _) in &MIME_TYPES { | |
| b.iter(|| { | |
| get_mime_type_str(mime_ext).expect(mime_ext); | |
| }); | |
| } | |
| } | |
| #[bench] | |
| fn bench_mime_str_uppercase(b: &mut Bencher) { | |
| let uppercased : Vec<_> = MIME_TYPES.into_iter().map(|(s, _)| s.to_uppercase()).collect(); | |
| for mime_ext in &uppercased { | |
| b.iter(|| { | |
| get_mime_type_str(mime_ext).expect(mime_ext); | |
| }); | |
| } | |
| } | |
| } |