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