rocket_http/header/media_type.rs
1use std::borrow::{Cow, Borrow};
2use std::str::FromStr;
3use std::fmt;
4use std::hash::{Hash, Hasher};
5
6use either::Either;
7
8use crate::ext::IntoCollection;
9use crate::uncased::UncasedStr;
10use crate::parse::{Indexed, IndexedStr, parse_media_type};
11
12use smallvec::SmallVec;
13
14/// An HTTP media type.
15///
16/// # Usage
17///
18/// A `MediaType` should rarely be used directly. Instead, one is typically used
19/// indirectly via types like [`Accept`](crate::Accept) and
20/// [`ContentType`](crate::ContentType), which internally contain `MediaType`s.
21/// Nonetheless, a `MediaType` can be created via the [`MediaType::new()`],
22/// [`MediaType::with_params()`], and [`MediaType::from_extension`()] methods.
23/// The preferred method, however, is to create a `MediaType` via an associated
24/// constant.
25///
26/// ## Example
27///
28/// A media type of `application/json` can be instantiated via the
29/// [`MediaType::JSON`] constant:
30///
31/// ```rust
32/// # extern crate rocket;
33/// use rocket::http::MediaType;
34///
35/// let json = MediaType::JSON;
36/// assert_eq!(json.top(), "application");
37/// assert_eq!(json.sub(), "json");
38///
39/// let json = MediaType::new("application", "json");
40/// assert_eq!(MediaType::JSON, json);
41/// ```
42///
43/// # Comparison and Hashing
44///
45/// The `PartialEq` and `Hash` implementations for `MediaType` _do not_ take
46/// into account parameters. This means that a media type of `text/html` is
47/// equal to a media type of `text/html; charset=utf-8`, for instance. This is
48/// typically the comparison that is desired.
49///
50/// If an exact comparison is desired that takes into account parameters, the
51/// [`exact_eq()`](MediaType::exact_eq()) method can be used.
52#[derive(Debug, Clone)]
53pub struct MediaType {
54 /// InitCell for the entire media type string.
55 pub(crate) source: Source,
56 /// The top-level type.
57 pub(crate) top: IndexedStr<'static>,
58 /// The subtype.
59 pub(crate) sub: IndexedStr<'static>,
60 /// The parameters, if any.
61 pub(crate) params: MediaParams
62}
63
64// FIXME: `Static` variant is needed for `const`. Need `const SmallVec::new`.
65#[derive(Debug, Clone)]
66pub(crate) enum MediaParams {
67 Static(&'static [(&'static str, &'static str)]),
68 Dynamic(SmallVec<[(IndexedStr<'static>, IndexedStr<'static>); 2]>)
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub(crate) enum Source {
73 Known(&'static str),
74 Custom(Cow<'static, str>),
75 None
76}
77
78impl From<Cow<'static, str>> for Source {
79 fn from(custom: Cow<'static, str>) -> Source {
80 Source::Custom(custom)
81 }
82}
83
84macro_rules! media_types {
85 ($($name:ident ($check:ident): $str:expr, $t:expr,
86 $s:expr $(; $k:expr => $v:expr)*,)+) => {
87 $(
88 /// Media Type for
89 #[doc = concat!("**", $str, "**: ")]
90 #[doc = concat!("`", $t, "/", $s, $("; ", $k, "=", $v,)* "`")]
91 #[allow(non_upper_case_globals)]
92 pub const $name: MediaType = MediaType::new_known(
93 concat!($t, "/", $s, $("; ", $k, "=", $v),*),
94 $t, $s, &[$(($k, $v)),*]
95 );
96 )+
97
98 /// Returns `true` if this MediaType is known to Rocket. In other words,
99 /// returns `true` if there is an associated constant for `self`.
100 pub fn is_known(&self) -> bool {
101 if let Source::Known(_) = self.source {
102 return true;
103 }
104
105 $(if self.$check() { return true })+
106 false
107 }
108
109 $(
110 /// Returns `true` if the top-level and sublevel types of
111 /// `self` are the same as those of
112 #[doc = concat!("`MediaType::", stringify!($name), "`, ")]
113 /// i.e
114 #[doc = concat!("`", $t, "/", $s, "`.")]
115 #[inline(always)]
116 pub fn $check(&self) -> bool {
117 *self == MediaType::$name
118 }
119 )+
120}}
121
122macro_rules! from_extension {
123 ($($ext:expr => $name:ident,)*) => (
124 /// Returns the Media Type associated with the extension `ext`.
125 ///
126 /// Extensions are matched case-insensitively. Not all extensions are
127 /// recognized. If an extensions is not recognized, `None` is returned.
128 /// The currently recognized extensions are:
129 ///
130 $(
131 #[doc = concat!("* ", $ext, " - [`MediaType::", stringify!($name), "`]")]
132 )*
133 ///
134 /// This list is likely to grow.
135 ///
136 /// # Example
137 ///
138 /// Recognized media types:
139 ///
140 /// ```rust
141 /// # extern crate rocket;
142 /// use rocket::http::MediaType;
143 ///
144 /// let xml = MediaType::from_extension("xml");
145 /// assert_eq!(xml, Some(MediaType::XML));
146 ///
147 /// let xml = MediaType::from_extension("XML");
148 /// assert_eq!(xml, Some(MediaType::XML));
149 /// ```
150 ///
151 /// An unrecognized media type:
152 ///
153 /// ```rust
154 /// # extern crate rocket;
155 /// use rocket::http::MediaType;
156 ///
157 /// let foo = MediaType::from_extension("foo");
158 /// assert!(foo.is_none());
159 /// ```
160 pub fn from_extension(ext: &str) -> Option<MediaType> {
161 match ext {
162 $(x if uncased::eq(x, $ext) => Some(MediaType::$name)),*,
163 _ => None
164 }
165 }
166 )
167}
168
169macro_rules! extension {
170 ($($ext:expr => $name:ident,)*) => (
171 /// Returns the most common file extension associated with the
172 /// Media-Type `self` if it is known. Otherwise, returns `None`.
173 ///
174 /// The currently recognized extensions are identical to those in
175 /// [`MediaType::from_extension()`] with the most common extension being
176 /// the first extension appearing in the list for a given Content-Type.
177 ///
178 /// # Example
179 ///
180 /// Known extension:
181 ///
182 /// ```rust
183 /// # extern crate rocket;
184 /// use rocket::http::MediaType;
185 ///
186 /// assert_eq!(MediaType::JSON.extension().unwrap(), "json");
187 /// assert_eq!(MediaType::JPEG.extension().unwrap(), "jpeg");
188 /// assert_eq!(MediaType::JPEG.extension().unwrap(), "JPEG");
189 /// assert_eq!(MediaType::PDF.extension().unwrap(), "pdf");
190 /// ```
191 ///
192 /// An unknown extension:
193 ///
194 /// ```rust
195 /// # extern crate rocket;
196 /// use rocket::http::MediaType;
197 ///
198 /// let foo = MediaType::new("foo", "bar");
199 /// assert!(foo.extension().is_none());
200 /// ```
201 #[inline]
202 pub fn extension(&self) -> Option<&UncasedStr> {
203 $(if self == &MediaType::$name { return Some($ext.into()) })*
204 None
205 }
206 )
207}
208
209macro_rules! parse_flexible {
210 ($($short:expr => $name:ident,)*) => (
211 /// Flexibly parses `name` into a [`MediaType`]. The parse is
212 /// _flexible_ because, in addition to strictly correct content types,
213 /// it recognizes the following shorthands:
214 ///
215 $(
216 #[doc = concat!("* ", $short, " - [`MediaType::", stringify!($name), "`]")]
217 )*
218 ///
219 /// For regular parsing, use [`MediaType::from_str()`].
220 ///
221 /// # Example
222 ///
223 /// Using a shorthand:
224 ///
225 /// ```rust
226 /// # extern crate rocket;
227 /// use rocket::http::MediaType;
228 ///
229 /// let html = MediaType::parse_flexible("html");
230 /// assert_eq!(html, Some(MediaType::HTML));
231 ///
232 /// let json = MediaType::parse_flexible("json");
233 /// assert_eq!(json, Some(MediaType::JSON));
234 /// ```
235 ///
236 /// Using the full media type:
237 ///
238 /// ```rust
239 /// # extern crate rocket;
240 /// use rocket::http::MediaType;
241 ///
242 /// let html = MediaType::parse_flexible("text/html; charset=utf-8");
243 /// assert_eq!(html, Some(MediaType::HTML));
244 ///
245 /// let json = MediaType::parse_flexible("application/json");
246 /// assert_eq!(json, Some(MediaType::JSON));
247 ///
248 /// let custom = MediaType::parse_flexible("application/x+custom");
249 /// assert_eq!(custom, Some(MediaType::new("application", "x+custom")));
250 /// ```
251 ///
252 /// An unrecognized media type:
253 ///
254 /// ```rust
255 /// # extern crate rocket;
256 /// use rocket::http::MediaType;
257 ///
258 /// let foo = MediaType::parse_flexible("foo");
259 /// assert_eq!(foo, None);
260 ///
261 /// let bar = MediaType::parse_flexible("foo/bar/baz");
262 /// assert_eq!(bar, None);
263 /// ```
264 pub fn parse_flexible(name: &str) -> Option<MediaType> {
265 match name {
266 $(x if uncased::eq(x, $short) => Some(MediaType::$name)),*,
267 _ => MediaType::from_str(name).ok(),
268 }
269 }
270 )
271}
272
273impl MediaType {
274 /// Creates a new `MediaType` with top-level type `top` and subtype `sub`.
275 /// This should _only_ be used to construct uncommon or custom media types.
276 /// Use an associated constant for everything else.
277 ///
278 /// # Example
279 ///
280 /// Create a custom `application/x-person` media type:
281 ///
282 /// ```rust
283 /// # extern crate rocket;
284 /// use rocket::http::MediaType;
285 ///
286 /// let custom = MediaType::new("application", "x-person");
287 /// assert_eq!(custom.top(), "application");
288 /// assert_eq!(custom.sub(), "x-person");
289 /// ```
290 #[inline]
291 pub fn new<T, S>(top: T, sub: S) -> MediaType
292 where T: Into<Cow<'static, str>>, S: Into<Cow<'static, str>>
293 {
294 MediaType {
295 source: Source::None,
296 top: Indexed::Concrete(top.into()),
297 sub: Indexed::Concrete(sub.into()),
298 params: MediaParams::Static(&[]),
299 }
300 }
301
302 /// Sets the parameters `parameters` on `self`.
303 ///
304 /// # Example
305 ///
306 /// Create a custom `application/x-id; id=1` media type:
307 ///
308 /// ```rust
309 /// # extern crate rocket;
310 /// use rocket::http::MediaType;
311 ///
312 /// let id = MediaType::new("application", "x-id").with_params(("id", "1"));
313 /// assert_eq!(id.to_string(), "application/x-id; id=1".to_string());
314 /// ```
315 ///
316 /// Create a custom `text/person; name=bob; weight=175` media type:
317 ///
318 /// ```rust
319 /// # extern crate rocket;
320 /// use rocket::http::MediaType;
321 ///
322 /// let mt = MediaType::new("text", "person")
323 /// .with_params([("name", "bob"), ("ref", "2382")]);
324 ///
325 /// assert_eq!(mt.to_string(), "text/person; name=bob; ref=2382".to_string());
326 /// ```
327 pub fn with_params<K, V, P>(mut self, ps: P) -> MediaType
328 where K: Into<Cow<'static, str>>,
329 V: Into<Cow<'static, str>>,
330 P: IntoCollection<(K, V)>
331 {
332 use Indexed::Concrete;
333
334 let params = ps.mapped(|(k, v)| (Concrete(k.into()), Concrete(v.into())));
335 self.params = MediaParams::Dynamic(params);
336 self
337 }
338
339 /// A `const` variant of [`MediaType::with_params()`]. Creates a new
340 /// `MediaType` with top-level type `top`, subtype `sub`, and parameters
341 /// `params`, which may be empty.
342 ///
343 /// # Example
344 ///
345 /// Create a custom `application/x-person` media type:
346 ///
347 /// ```rust
348 /// use rocket::http::MediaType;
349 ///
350 /// let custom = MediaType::const_new("application", "x-person", &[]);
351 /// assert_eq!(custom.top(), "application");
352 /// assert_eq!(custom.sub(), "x-person");
353 /// ```
354 #[inline]
355 pub const fn const_new(
356 top: &'static str,
357 sub: &'static str,
358 params: &'static [(&'static str, &'static str)]
359 ) -> MediaType {
360 MediaType {
361 source: Source::None,
362 top: Indexed::Concrete(Cow::Borrowed(top)),
363 sub: Indexed::Concrete(Cow::Borrowed(sub)),
364 params: MediaParams::Static(params),
365 }
366 }
367
368 #[inline]
369 pub(crate) const fn new_known(
370 source: &'static str,
371 top: &'static str,
372 sub: &'static str,
373 params: &'static [(&'static str, &'static str)]
374 ) -> MediaType {
375 MediaType {
376 source: Source::Known(source),
377 top: Indexed::Concrete(Cow::Borrowed(top)),
378 sub: Indexed::Concrete(Cow::Borrowed(sub)),
379 params: MediaParams::Static(params),
380 }
381 }
382
383 pub(crate) fn known_source(&self) -> Option<&'static str> {
384 match self.source {
385 Source::Known(string) => Some(string),
386 Source::Custom(Cow::Borrowed(string)) => Some(string),
387 _ => None
388 }
389 }
390
391 known_shorthands!(parse_flexible);
392
393 known_extensions!(from_extension);
394
395 /// Returns the top-level type for this media type. The return type,
396 /// `UncasedStr`, has caseless equality comparison and hashing.
397 ///
398 /// # Example
399 ///
400 /// ```rust
401 /// # extern crate rocket;
402 /// use rocket::http::MediaType;
403 ///
404 /// let plain = MediaType::Plain;
405 /// assert_eq!(plain.top(), "text");
406 /// assert_eq!(plain.top(), "TEXT");
407 /// assert_eq!(plain.top(), "Text");
408 /// ```
409 #[inline]
410 pub fn top(&self) -> &UncasedStr {
411 self.top.from_source(self.source.as_str()).into()
412 }
413
414 /// Returns the subtype for this media type. The return type,
415 /// `UncasedStr`, has caseless equality comparison and hashing.
416 ///
417 /// # Example
418 ///
419 /// ```rust
420 /// # extern crate rocket;
421 /// use rocket::http::MediaType;
422 ///
423 /// let plain = MediaType::Plain;
424 /// assert_eq!(plain.sub(), "plain");
425 /// assert_eq!(plain.sub(), "PlaIN");
426 /// assert_eq!(plain.sub(), "pLaIn");
427 /// ```
428 #[inline]
429 pub fn sub(&self) -> &UncasedStr {
430 self.sub.from_source(self.source.as_str()).into()
431 }
432
433 /// Returns a `u8` representing how specific the top-level type and subtype
434 /// of this media type are.
435 ///
436 /// The return value is either `0`, `1`, or `2`, where `2` is the most
437 /// specific. A `0` is returned when both the top and sublevel types are
438 /// `*`. A `1` is returned when only one of the top or sublevel types is
439 /// `*`, and a `2` is returned when neither the top or sublevel types are
440 /// `*`.
441 ///
442 /// # Example
443 ///
444 /// ```rust
445 /// # extern crate rocket;
446 /// use rocket::http::MediaType;
447 ///
448 /// let mt = MediaType::Plain;
449 /// assert_eq!(mt.specificity(), 2);
450 ///
451 /// let mt = MediaType::new("text", "*");
452 /// assert_eq!(mt.specificity(), 1);
453 ///
454 /// let mt = MediaType::Any;
455 /// assert_eq!(mt.specificity(), 0);
456 /// ```
457 #[inline]
458 pub fn specificity(&self) -> u8 {
459 (self.top() != "*") as u8 + (self.sub() != "*") as u8
460 }
461
462 /// Compares `self` with `other` and returns `true` if `self` and `other`
463 /// are exactly equal to each other, including with respect to their
464 /// parameters and their order.
465 ///
466 /// This is different from the `PartialEq` implementation in that it
467 /// considers parameters. In particular, `Eq` implies `PartialEq` but
468 /// `PartialEq` does not imply `Eq`. That is, if `PartialEq` returns false,
469 /// this function is guaranteed to return false. Similarly, if `exact_eq`
470 /// returns `true`, `PartialEq` is guaranteed to return true. However, if
471 /// `PartialEq` returns `true`, `exact_eq` function may or may not return
472 /// `true`.
473 ///
474 /// # Example
475 ///
476 /// ```rust
477 /// # extern crate rocket;
478 /// use rocket::http::MediaType;
479 ///
480 /// let plain = MediaType::Plain;
481 /// let plain2 = MediaType::new("text", "plain").with_params(("charset", "utf-8"));
482 /// let just_plain = MediaType::new("text", "plain");
483 ///
484 /// // The `PartialEq` implementation doesn't consider parameters.
485 /// assert!(plain == just_plain);
486 /// assert!(just_plain == plain2);
487 /// assert!(plain == plain2);
488 ///
489 /// // While `exact_eq` does.
490 /// assert!(!plain.exact_eq(&just_plain));
491 /// assert!(!plain2.exact_eq(&just_plain));
492 /// assert!(plain.exact_eq(&plain2));
493 /// ```
494 pub fn exact_eq(&self, other: &MediaType) -> bool {
495 self == other && self.params().eq(other.params())
496 }
497
498 /// Returns an iterator over the (key, value) pairs of the media type's
499 /// parameter list. The iterator will be empty if the media type has no
500 /// parameters.
501 ///
502 /// # Example
503 ///
504 /// The `MediaType::Plain` type has one parameter: `charset=utf-8`:
505 ///
506 /// ```rust
507 /// # extern crate rocket;
508 /// use rocket::http::MediaType;
509 ///
510 /// let plain = MediaType::Plain;
511 /// let (key, val) = plain.params().next().unwrap();
512 /// assert_eq!(key, "charset");
513 /// assert_eq!(val, "utf-8");
514 /// ```
515 ///
516 /// The `MediaType::PNG` type has no parameters:
517 ///
518 /// ```rust
519 /// # extern crate rocket;
520 /// use rocket::http::MediaType;
521 ///
522 /// let png = MediaType::PNG;
523 /// assert_eq!(png.params().count(), 0);
524 /// ```
525 #[inline]
526 pub fn params(&self) -> impl Iterator<Item=(&'_ UncasedStr, &'_ str)> + '_ {
527 let raw = match self.params {
528 MediaParams::Static(slice) => Either::Left(slice.iter().cloned()),
529 MediaParams::Dynamic(ref vec) => {
530 Either::Right(vec.iter().map(move |&(ref key, ref val)| {
531 let source_str = self.source.as_str();
532 (key.from_source(source_str), val.from_source(source_str))
533 }))
534 }
535 };
536
537 raw.map(|(k, v)| (k.into(), v))
538 }
539
540 /// Returns the first parameter with name `name`, if there is any.
541 #[inline]
542 pub fn param<'a>(&'a self, name: &str) -> Option<&'a str> {
543 self.params()
544 .filter(|(k, _)| *k == name)
545 .map(|(_, v)| v)
546 .next()
547 }
548
549 known_extensions!(extension);
550
551 known_media_types!(media_types);
552}
553
554impl FromStr for MediaType {
555 // Ideally we'd return a `ParseError`, but that requires a lifetime.
556 type Err = String;
557
558 #[inline]
559 fn from_str(raw: &str) -> Result<MediaType, String> {
560 parse_media_type(raw).map_err(|e| e.to_string())
561 }
562}
563
564impl PartialEq for MediaType {
565 #[inline(always)]
566 fn eq(&self, other: &MediaType) -> bool {
567 self.top() == other.top() && self.sub() == other.sub()
568 }
569}
570
571impl Eq for MediaType { }
572
573impl Hash for MediaType {
574 #[inline]
575 fn hash<H: Hasher>(&self, state: &mut H) {
576 self.top().hash(state);
577 self.sub().hash(state);
578 }
579}
580
581impl fmt::Display for MediaType {
582 #[inline]
583 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
584 if let Some(src) = self.known_source() {
585 src.fmt(f)
586 } else {
587 write!(f, "{}/{}", self.top(), self.sub())?;
588 for (key, val) in self.params() {
589 write!(f, "; {}={}", key, val)?;
590 }
591
592 Ok(())
593 }
594 }
595}
596
597impl Default for MediaParams {
598 fn default() -> Self {
599 MediaParams::Dynamic(SmallVec::new())
600 }
601}
602
603impl Extend<(IndexedStr<'static>, IndexedStr<'static>)> for MediaParams {
604 fn extend<T>(&mut self, iter: T)
605 where T: IntoIterator<Item = (IndexedStr<'static>, IndexedStr<'static>)>
606 {
607 match self {
608 MediaParams::Static(..) => panic!("can't add to static collection!"),
609 MediaParams::Dynamic(ref mut v) => v.extend(iter)
610 }
611 }
612}
613
614impl Source {
615 #[inline]
616 fn as_str(&self) -> Option<&str> {
617 match *self {
618 Source::Known(s) => Some(s),
619 Source::Custom(ref s) => Some(s.borrow()),
620 Source::None => None
621 }
622 }
623}