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 
25 pub use mime::Mime;
26 
27 use std::ffi::OsStr;
28 use std::iter::FusedIterator;
29 use std::path::Path;
30 use std::{iter, slice};
31 
32 #[cfg(feature = "phf")]
33 #[path = "impl_phf.rs"]
34 mod impl_;
35 
36 #[cfg(not(feature = "phf"))]
37 #[path = "impl_bin_search.rs"]
38 mod 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
54 pub struct MimeGuess(&'static [&'static str]);
55 
56 impl 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.
from_ext(ext: &str) -> MimeGuess63     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
from_path<P: AsRef<Path>>(path: P) -> MimeGuess86     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.
is_empty(&self) -> bool94     pub fn is_empty(&self) -> bool {
95         self.0.is_empty()
96     }
97 
98     /// Get the number of MIME types in the current guess.
count(&self) -> usize99     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.
first(&self) -> Option<Mime>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.
first_raw(&self) -> Option<&'static str>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/content-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/content-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
first_or_octet_stream(&self) -> Mime135     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.
first_or_text_plain(&self) -> Mime143     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.
first_or(&self, default: Mime) -> Mime150     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.
first_or_else<F>(&self, default_fn: F) -> Mime where F: FnOnce() -> Mime,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.
iter(&self) -> Iter168     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.
iter_raw(&self) -> IterRaw175     pub fn iter_raw(&self) -> IterRaw {
176         IterRaw(self.0.iter().cloned())
177     }
178 }
179 
180 impl IntoIterator for MimeGuess {
181     type Item = Mime;
182     type IntoIter = Iter;
183 
into_iter(self) -> Self::IntoIter184     fn into_iter(self) -> Self::IntoIter {
185         self.iter()
186     }
187 }
188 
189 impl<'a> IntoIterator for &'a MimeGuess {
190     type Item = Mime;
191     type IntoIter = Iter;
192 
into_iter(self) -> Self::IntoIter193     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)]
202 pub struct Iter(iter::Map<IterRaw, fn(&'static str) -> Mime>);
203 
204 impl Iterator for Iter {
205     type Item = Mime;
206 
next(&mut self) -> Option<Self::Item>207     fn next(&mut self) -> Option<Self::Item> {
208         self.0.next()
209     }
210 
size_hint(&self) -> (usize, Option<usize>)211     fn size_hint(&self) -> (usize, Option<usize>) {
212         self.0.size_hint()
213     }
214 }
215 
216 impl DoubleEndedIterator for Iter {
next_back(&mut self) -> Option<Self::Item>217     fn next_back(&mut self) -> Option<Self::Item> {
218         self.0.next_back()
219     }
220 }
221 
222 impl FusedIterator for Iter {}
223 
224 impl ExactSizeIterator for Iter {
len(&self) -> usize225     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)]
234 pub struct IterRaw(iter::Cloned<slice::Iter<'static, &'static str>>);
235 
236 impl Iterator for IterRaw {
237     type Item = &'static str;
238 
next(&mut self) -> Option<Self::Item>239     fn next(&mut self) -> Option<Self::Item> {
240         self.0.next()
241     }
242 
size_hint(&self) -> (usize, Option<usize>)243     fn size_hint(&self) -> (usize, Option<usize>) {
244         self.0.size_hint()
245     }
246 }
247 
248 impl DoubleEndedIterator for IterRaw {
next_back(&mut self) -> Option<Self::Item>249     fn next_back(&mut self) -> Option<Self::Item> {
250         self.0.next_back()
251     }
252 }
253 
254 impl FusedIterator for IterRaw {}
255 
256 impl ExactSizeIterator for IterRaw {
len(&self) -> usize257     fn len(&self) -> usize {
258         self.0.len()
259     }
260 }
261 
expect_mime(s: &str) -> Mime262 fn expect_mime(s: &str) -> Mime {
263     // `.parse()` should be checked at compile time to never fail
264     s.parse()
265         .unwrap_or_else(|e| panic!("failed to parse media-type {:?}: {}", s, e))
266 }
267 
268 /// Wrapper of [`MimeGuess::from_ext()`](struct.MimeGuess.html#method.from_ext).
from_ext(ext: &str) -> MimeGuess269 pub 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).
from_path<P: AsRef<Path>>(path: P) -> MimeGuess274 pub 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/content-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 )]
guess_mime_type<P: AsRef<Path>>(path: P) -> Mime297 pub 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")]
guess_mime_type_opt<P: AsRef<Path>>(path: P) -> Option<Mime>307 pub 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")]
mime_str_for_path_ext<P: AsRef<Path>>(path: P) -> Option<&'static str>322 pub 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/content-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 )]
get_mime_type(search_ext: &str) -> Mime340 pub 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")]
get_mime_type_opt(search_ext: &str) -> Option<Mime>349 pub 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 )]
get_mime_type_str(search_ext: &str) -> Option<&'static str>363 pub 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")]
get_mime_extensions(mime: &Mime) -> Option<&'static [&'static str]>378 pub fn get_mime_extensions(mime: &Mime) -> Option<&'static [&'static str]> {
379     get_extensions(mime.type_().as_ref(), 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")]
get_mime_extensions_str(mut mime_str: &str) -> Option<&'static [&'static str]>396 pub 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) = mime_str.find(';') {
400         mime_str = &mime_str[..sep_idx];
401     }
402 
403     let (top, sub) = {
404         let split_idx = mime_str.find('/').unwrap();
405         (&mime_str[..split_idx], &mime_str[split_idx + 1..])
406     };
407 
408     get_extensions(top, 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")]
get_extensions(toplevel: &str, sublevel: &str) -> Option<&'static [&'static str]>421 pub 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")]
octet_stream() -> Mime427 pub fn octet_stream() -> Mime {
428     "application/octet-stream".parse().unwrap()
429 }
430 
431 #[cfg(test)]
432 mod tests {
433     include!("mime_types.rs");
434 
435     use super::{from_ext, from_path, expect_mime};
436     #[allow(deprecated, unused_imports)]
437     use std::ascii::AsciiExt;
438 
439     use std::fmt::Debug;
440     use std::path::Path;
441 
442 
443     #[test]
check_type_bounds()444     fn check_type_bounds() {
445         fn assert_type_bounds<T: Clone + Debug + Send + Sync + 'static>() {}
446 
447         assert_type_bounds::<super::MimeGuess>();
448         assert_type_bounds::<super::Iter>();
449         assert_type_bounds::<super::IterRaw>();
450     }
451 
452     #[test]
test_mime_type_guessing()453     fn test_mime_type_guessing() {
454         assert_eq!(
455             from_ext("gif").first_or_octet_stream().to_string(),
456             "image/gif".to_string()
457         );
458         assert_eq!(
459             from_ext("TXT").first_or_octet_stream().to_string(),
460             "text/plain".to_string()
461         );
462         assert_eq!(
463             from_ext("blahblah").first_or_octet_stream().to_string(),
464             "application/octet-stream".to_string()
465         );
466 
467         assert_eq!(
468             from_path(Path::new("/path/to/file.gif"))
469                 .first_or_octet_stream()
470                 .to_string(),
471             "image/gif".to_string()
472         );
473         assert_eq!(
474             from_path("/path/to/file.gif").first_or_octet_stream().to_string(),
475             "image/gif".to_string()
476         );
477     }
478 
479     #[test]
test_mime_type_guessing_opt()480     fn test_mime_type_guessing_opt() {
481         assert_eq!(
482             from_ext("gif").first().unwrap().to_string(),
483             "image/gif".to_string()
484         );
485         assert_eq!(
486             from_ext("TXT").first().unwrap().to_string(),
487             "text/plain".to_string()
488         );
489         assert_eq!(from_ext("blahblah").first(), None);
490 
491         assert_eq!(
492             from_path("/path/to/file.gif").first().unwrap().to_string(),
493             "image/gif".to_string()
494         );
495         assert_eq!(from_path("/path/to/file").first(), None);
496     }
497 
498     #[test]
test_are_mime_types_parseable()499     fn test_are_mime_types_parseable() {
500         for (_, mimes) in MIME_TYPES {
501             mimes.iter().for_each(|s| { expect_mime(s); });
502         }
503     }
504 
505     // RFC: Is this test necessary anymore? --@cybergeek94, 2/1/2016
506     #[test]
test_are_extensions_ascii()507     fn test_are_extensions_ascii() {
508         for (ext, _) in MIME_TYPES {
509             assert!(ext.is_ascii(), "Extension not ASCII: {:?}", ext);
510         }
511     }
512 
513     #[test]
test_are_extensions_sorted()514     fn test_are_extensions_sorted() {
515         // simultaneously checks the requirement that duplicate extension entries are adjacent
516         for (&(ext, _), &(n_ext, _)) in MIME_TYPES.iter().zip(MIME_TYPES.iter().skip(1)) {
517             assert!(
518                 ext <= n_ext,
519                 "Extensions in src/mime_types should be sorted lexicographically
520                 in ascending order. Failed assert: {:?} <= {:?}",
521                 ext,
522                 n_ext
523             );
524         }
525     }
526 }
527