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}