media_type/
media_guess.rs1use core::{cmp::Ordering, iter, slice};
4
5#[cfg(any(feature = "alloc", feature = "reverse-lookup"))]
6use crate::MediaType;
7
8include!("media_types.rs");
9
10#[cfg(feature = "reverse-lookup")]
11include!(env!("REVERSE_LOOKUP_PATH"));
12
13#[cfg(feature = "reverse-lookup")]
14#[derive(Copy, Clone)]
15struct TopLevelExts {
16 start: usize,
17 end: usize,
18 subs: &'static [(&'static str, (usize, usize))],
19}
20
21fn get_media_types(ext: &str) -> Option<&'static [&'static str]> {
22 map_lookup(MEDIA_TYPES, ext)
23}
24
25#[cfg(feature = "reverse-lookup")]
36pub fn get_extensions(toplevel: &str, sublevel: &str) -> Option<&'static [&'static str]> {
37 if toplevel == "*" {
38 return Some(EXTS);
39 }
40
41 let top = map_lookup(REV_MAPPINGS, toplevel)?;
42
43 if sublevel == "*" {
44 return Some(&EXTS[top.start..top.end]);
45 }
46
47 let sub = map_lookup(top.subs, sublevel)?;
48 Some(&EXTS[sub.0..sub.1])
49}
50
51#[cfg(feature = "reverse-lookup")]
57pub fn get_media_extensions(media_type: &MediaType) -> Option<&'static [&'static str]> {
58 get_extensions(media_type.type_(), media_type.subtype())
59}
60
61#[cfg(feature = "reverse-lookup")]
68pub fn get_media_extensions_str(mut media_type: &str) -> Option<&'static [&'static str]> {
69 media_type = media_type.trim();
70
71 if let Some(separator) = media_type.find(';') {
72 media_type = &media_type[..separator];
73 }
74
75 let (toplevel, sublevel) = {
76 let slash = media_type.find('/')?;
77 (&media_type[..slash], &media_type[slash + 1..])
78 };
79
80 get_extensions(toplevel, sublevel)
81}
82
83#[derive(Copy, Clone, Debug, PartialEq, Eq)]
96pub struct MediaTypeGuess(&'static [&'static str]);
97
98impl MediaTypeGuess {
99 pub fn from_ext(ext: &str) -> MediaTypeGuess {
113 if ext.is_empty() {
114 return MediaTypeGuess(&[]);
115 }
116
117 get_media_types(ext).map_or(MediaTypeGuess(&[]), MediaTypeGuess)
118 }
119
120 #[cfg(feature = "std")]
131 pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> MediaTypeGuess {
132 path.as_ref()
133 .extension()
134 .and_then(std::ffi::OsStr::to_str)
135 .map_or(MediaTypeGuess(&[]), MediaTypeGuess::from_ext)
136 }
137
138 pub fn is_empty(&self) -> bool {
140 self.0.is_empty()
141 }
142
143 pub fn count(&self) -> usize {
145 self.0.len()
146 }
147
148 #[cfg(feature = "alloc")]
150 pub fn first(&self) -> Option<MediaType> {
151 self.first_raw().map(expect_media_type)
152 }
153
154 pub fn first_raw(&self) -> Option<&'static str> {
156 self.0.first().copied()
157 }
158
159 #[cfg(feature = "alloc")]
163 pub fn first_or_octet_stream(&self) -> MediaType {
164 self.first_or(crate::APPLICATION_OCTET_STREAM)
165 }
166
167 #[cfg(feature = "alloc")]
171 pub fn first_or_text_plain(&self) -> MediaType {
172 self.first_or(crate::TEXT_PLAIN)
173 }
174
175 #[cfg(feature = "alloc")]
177 pub fn first_or(&self, default: MediaType) -> MediaType {
178 self.first().unwrap_or(default)
179 }
180
181 #[cfg(feature = "alloc")]
183 pub fn first_or_else<F>(&self, default: F) -> MediaType
184 where
185 F: FnOnce() -> MediaType,
186 {
187 self.first().unwrap_or_else(default)
188 }
189
190 #[cfg(feature = "alloc")]
192 pub fn iter(&self) -> Iter {
193 Iter(self.iter_raw().map(expect_media_type))
194 }
195
196 pub fn iter_raw(&self) -> IterRaw {
198 IterRaw(self.0.iter().copied())
199 }
200}
201
202#[cfg(feature = "alloc")]
203impl IntoIterator for MediaTypeGuess {
204 type Item = MediaType;
205 type IntoIter = Iter;
206
207 fn into_iter(self) -> Self::IntoIter {
208 self.iter()
209 }
210}
211
212#[cfg(feature = "alloc")]
213impl<'a> IntoIterator for &'a MediaTypeGuess {
214 type Item = MediaType;
215 type IntoIter = Iter;
216
217 fn into_iter(self) -> Self::IntoIter {
218 self.iter()
219 }
220}
221
222#[cfg(feature = "alloc")]
224#[derive(Clone, Debug)]
225pub struct Iter(iter::Map<IterRaw, fn(&'static str) -> MediaType>);
226
227#[cfg(feature = "alloc")]
228impl Iterator for Iter {
229 type Item = MediaType;
230
231 fn next(&mut self) -> Option<Self::Item> {
232 self.0.next()
233 }
234
235 fn size_hint(&self) -> (usize, Option<usize>) {
236 self.0.size_hint()
237 }
238}
239
240#[cfg(feature = "alloc")]
241impl DoubleEndedIterator for Iter {
242 fn next_back(&mut self) -> Option<Self::Item> {
243 self.0.next_back()
244 }
245}
246
247#[cfg(feature = "alloc")]
248impl ExactSizeIterator for Iter {
249 fn len(&self) -> usize {
250 self.0.len()
251 }
252}
253
254#[cfg(feature = "alloc")]
255impl iter::FusedIterator for Iter {}
256
257#[derive(Clone, Debug)]
259pub struct IterRaw(iter::Copied<slice::Iter<'static, &'static str>>);
260
261impl Iterator for IterRaw {
262 type Item = &'static str;
263
264 fn next(&mut self) -> Option<Self::Item> {
265 self.0.next()
266 }
267
268 fn size_hint(&self) -> (usize, Option<usize>) {
269 self.0.size_hint()
270 }
271}
272
273impl DoubleEndedIterator for IterRaw {
274 fn next_back(&mut self) -> Option<Self::Item> {
275 self.0.next_back()
276 }
277}
278
279impl ExactSizeIterator for IterRaw {
280 fn len(&self) -> usize {
281 self.0.len()
282 }
283}
284
285impl iter::FusedIterator for IterRaw {}
286
287pub fn from_ext(ext: &str) -> MediaTypeGuess {
291 MediaTypeGuess::from_ext(ext)
292}
293
294#[cfg(feature = "std")]
298pub fn from_path<P: AsRef<std::path::Path>>(path: P) -> MediaTypeGuess {
299 MediaTypeGuess::from_path(path)
300}
301
302#[cfg(feature = "alloc")]
303fn expect_media_type(s: &str) -> MediaType {
304 s.parse()
306 .unwrap_or_else(|error| panic!("failed to parse media type {:?}: {}", s, error))
307}
308
309fn map_lookup<K, V>(map: &'static [(K, V)], key: &str) -> Option<V>
314where
315 K: Copy + Into<&'static str>,
316 V: Copy,
317{
318 let mut left = 0;
319 let mut right = map.len();
320
321 while left < right {
322 let mid = left + (right - left) / 2;
323 match cmp_ignore_ascii_case(map[mid].0.into(), key) {
324 Ordering::Less => left = mid + 1,
325 Ordering::Greater => right = mid,
326 Ordering::Equal => return Some(map[mid].1),
327 }
328 }
329
330 None
331}
332
333fn cmp_ignore_ascii_case(a: &str, b: &str) -> Ordering {
335 let (a, b) = (a.as_bytes(), b.as_bytes());
336 let common = a.len().min(b.len());
337
338 for i in 0..common {
339 let (ca, cb) = (a[i].to_ascii_lowercase(), b[i].to_ascii_lowercase());
340 if ca != cb {
341 return ca.cmp(&cb);
342 }
343 }
344
345 a.len().cmp(&b.len())
346}
347
348#[cfg(test)]
349mod tests {
350 #![allow(clippy::needless_borrow)]
351
352 #[cfg(feature = "alloc")]
353 use alloc::string::ToString;
354
355 use super::*;
356
357 #[test]
358 fn test_type_bounds() {
359 fn assert_bounds<T: Clone + core::fmt::Debug + Send + Sync + 'static>() {}
360
361 assert_bounds::<MediaTypeGuess>();
362 assert_bounds::<IterRaw>();
363 #[cfg(feature = "alloc")]
364 assert_bounds::<Iter>();
365 }
366
367 #[test]
368 fn test_are_extensions_ascii() {
369 for (ext, _) in MEDIA_TYPES {
370 assert!(ext.is_ascii(), "extension is not ASCII: {:?}", ext);
371 }
372 }
373
374 #[test]
375 fn test_are_extensions_sorted() {
376 for (&(ext, _), &(next, _)) in MEDIA_TYPES.iter().zip(MEDIA_TYPES.iter().skip(1)) {
378 assert!(
379 ext <= next,
380 "extensions in media_types.rs must be sorted lexicographically: {:?} <= {:?}",
381 ext,
382 next
383 );
384 }
385 }
386
387 #[cfg(feature = "alloc")]
388 #[test]
389 fn test_are_media_types_parseable() {
390 for (_, media_types) in MEDIA_TYPES {
391 for value in *media_types {
392 expect_media_type(value);
393 }
394 }
395 }
396
397 #[cfg(feature = "alloc")]
398 #[test]
399 fn test_media_type_guessing() {
400 assert_eq!(from_ext("gif").first_or_octet_stream().to_string(), "image/gif");
401 assert_eq!(from_ext("TXT").first_or_octet_stream().to_string(), "text/plain");
402 assert_eq!(
403 from_ext("blahblah").first_or_octet_stream().to_string(),
404 "application/octet-stream"
405 );
406 assert_eq!(from_ext("gif").first().unwrap().to_string(), "image/gif");
407 assert_eq!(from_ext("blahblah").first(), None);
408 assert_eq!(from_ext("zst").first(), Some(crate::APPLICATION_ZSTD));
409 }
410
411 #[cfg(all(feature = "std", feature = "alloc"))]
412 #[test]
413 fn test_media_type_guessing_from_path() {
414 assert_eq!(from_path("/path/to/file.gif").first(), Some(crate::IMAGE_GIF));
415 assert_eq!(from_path("file").first(), None);
416 }
417
418 #[cfg(feature = "reverse-lookup")]
419 #[test]
420 fn test_get_media_extensions_str_no_panic_if_bad_media_type() {
421 assert_eq!(get_media_extensions_str(""), None);
422 }
423
424 #[cfg(feature = "reverse-lookup")]
425 #[test]
426 fn test_reverse_mappings() {
427 assert!(get_extensions("image", "png").unwrap().contains(&"png"));
428 assert!(get_extensions("image", "*").unwrap().contains(&"png"));
429 assert!(get_extensions("*", "*").unwrap().contains(&"png"));
430 assert_eq!(get_extensions("nope", "nope"), None);
431 assert!(get_media_extensions(&crate::IMAGE_PNG).unwrap().contains(&"png"));
432 }
433}