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}