Revision control

Copy as Markdown

Other Tools

//! 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 <br /> (`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.
///
pub fn from_path<P: AsRef<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<Mime> {
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])
///
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<F>(&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<IterRaw, fn(&'static str) -> Mime>);
impl Iterator for Iter {
type Item = Mime;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for Iter {
fn next_back(&mut self) -> Option<Self::Item> {
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<slice::Iter<'static, &'static str>>);
impl Iterator for IterRaw {
type Item = &'static str;
fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.0.size_hint()
}
}
impl DoubleEndedIterator for IterRaw {
fn next_back(&mut self) -> Option<Self::Item> {
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<P: AsRef<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`.
///
#[deprecated(
since = "2.0.0",
note = "Use `from_path(path).first_or_octet_stream()` instead"
)]
pub fn guess_mime_type<P: AsRef<Path>>(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<P: AsRef<Path>>(path: P) -> Option<Mime> {
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<P: AsRef<Path>>(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`.
///
#[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<Mime> {
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 `<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.
#[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 `<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).
#[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<T: Clone + Debug + Send + Sync + 'static>() {}
assert_type_bounds::<super::MimeGuess>();
assert_type_bounds::<super::Iter>();
assert_type_bounds::<super::IterRaw>();
}
#[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);
}
}