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: FromStrError| 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(toplevel:mime.type_().as_ref(), sublevel: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: usize) = mime_str.find(';') {
400 mime_str = &mime_str[..sep_idx];
401 }
402
403 let (top: &str, sub: &str) = {
404 let split_idx: usize = mime_str.find('/')?;
405 (&mime_str[..split_idx], &mime_str[split_idx + 1..])
406 };
407
408 get_extensions(toplevel:top, sublevel: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}
535