Skip to main content

http_types_rs/mime/
mod.rs

1//! IANA Media Types.
2//!
3//! [Read more](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types).
4
5mod constants;
6mod parse;
7
8pub use constants::*;
9
10use std::borrow::Cow;
11use std::fmt::{self, Debug, Display};
12use std::option;
13use std::str::FromStr;
14
15use crate::headers::{HeaderValue, ToHeaderValues};
16
17use infer::Infer;
18
19/// An IANA media type.
20///
21/// ```
22/// use http_types_rs::mime::Mime;
23/// use std::str::FromStr;
24///
25/// let mime = Mime::from_str("text/html;charset=utf-8").unwrap();
26/// assert_eq!(mime.essence(), "text/html");
27/// assert_eq!(mime.param("charset").unwrap(), "utf-8");
28/// ```
29// NOTE: we cannot statically initialize Strings with values yet, so we keep dedicated static
30// fields for the static strings.
31#[derive(Clone, PartialEq, Eq, Debug)]
32pub struct Mime {
33    pub(crate) essence: Cow<'static, str>,
34    pub(crate) basetype: Cow<'static, str>,
35    pub(crate) subtype: Cow<'static, str>,
36    // NOTE(yosh): this is a hack because we can't populate vecs in const yet.
37    // This enables us to encode media types as utf-8 at compilation.
38    pub(crate) is_utf8: bool,
39    pub(crate) params: Vec<(ParamName, ParamValue)>,
40}
41
42impl Mime {
43    /// Sniff the mime type from a byte slice.
44    pub fn sniff(bytes: &[u8]) -> crate::Result<Self> {
45        let info = Infer::new();
46        let mime = match info.get(bytes) {
47            Some(info) => info.mime_type(),
48            None => crate::bail!("Could not sniff the mime type"),
49        };
50        Mime::from_str(mime)
51    }
52
53    /// Guess the mime type from a file extension
54    pub fn from_extension(extension: impl AsRef<str>) -> Option<Self> {
55        match extension.as_ref() {
56            "7z" => Some(SEVENZIP),
57            "atom" => Some(ATOM),
58            "avi" => Some(AVI),
59            "bin" | "exe" | "dll" | "iso" | "img" => Some(BYTE_STREAM),
60            "bmp" => Some(BMP),
61            "css" => Some(CSS),
62            "html" => Some(HTML),
63            "ico" => Some(ICO),
64            "js" | "mjs" | "jsonp" => Some(JAVASCRIPT),
65            "json" => Some(JSON),
66            "m4a" => Some(M4A),
67            "mid" | "midi" | "kar" => Some(MIDI),
68            "mp3" => Some(MP3),
69            "mp4" => Some(MP4),
70            "mpeg" | "mpg" => Some(MPEG),
71            "ogg" => Some(OGG),
72            "otf" => Some(OTF),
73            "rss" => Some(RSS),
74            "svg" | "svgz" => Some(SVG),
75            "ttf" => Some(TTF),
76            "txt" => Some(PLAIN),
77            "wasm" => Some(WASM),
78            "webm" => Some(WEBM),
79            "webp" => Some(WEBP),
80            "woff" => Some(WOFF),
81            "woff2" => Some(WOFF2),
82            "xml" => Some(XML),
83            "zip" => Some(ZIP),
84            _ => None,
85        }
86    }
87
88    /// Access the Mime's `type` value.
89    ///
90    /// According to the spec this method should be named `type`, but that's a reserved keyword in
91    /// Rust so hence prefix with `base` instead.
92    pub fn basetype(&self) -> &str {
93        &self.basetype
94    }
95
96    /// Access the Mime's `subtype` value.
97    pub fn subtype(&self) -> &str {
98        &self.subtype
99    }
100
101    /// Access the Mime's `essence` value.
102    pub fn essence(&self) -> &str {
103        &self.essence
104    }
105
106    /// Get a reference to a param.
107    pub fn param(&self, name: impl Into<ParamName>) -> Option<&ParamValue> {
108        let name: ParamName = name.into();
109        if name.as_str() == "charset" && self.is_utf8 {
110            return Some(&ParamValue(Cow::Borrowed("utf-8")));
111        }
112
113        self.params.iter().find(|(k, _)| k == &name).map(|(_, v)| v)
114    }
115
116    /// Remove a param from the set. Returns the `ParamValue` if it was contained within the set.
117    pub fn remove_param(&mut self, name: impl Into<ParamName>) -> Option<ParamValue> {
118        let name: ParamName = name.into();
119        if name.as_str() == "charset" && self.is_utf8 {
120            self.is_utf8 = false;
121            return Some(ParamValue(Cow::Borrowed("utf-8")));
122        }
123        self.params.iter().position(|(k, _)| k == &name).map(|pos| self.params.remove(pos).1)
124    }
125
126    /// Check if this mime is a subtype of another mime.
127    ///
128    /// # Examples
129    ///
130    /// ```
131    /// // All mime types are subsets of */*
132    /// use http_types_rs::mime::Mime;
133    /// use std::str::FromStr;
134    ///
135    /// assert!(Mime::from_str("text/css").unwrap().subset_eq(&Mime::from_str("*/*").unwrap()));
136    ///
137    /// // A mime type is subset of itself
138    /// assert!(Mime::from_str("text/css").unwrap().subset_eq(&Mime::from_str("text/css").unwrap()));
139    ///
140    /// // A mime type which is otherwise a subset with extra parameters is a subset of a mime type without those parameters
141    /// assert!(Mime::from_str("text/css;encoding=utf-8").unwrap().subset_eq(&Mime::from_str("text/css").unwrap()));
142    ///
143    /// // A mime type more general than another mime type is not a subset
144    /// assert!(!Mime::from_str("*/css;encoding=utf-8").unwrap().subset_eq(&Mime::from_str("text/css").unwrap()));
145    /// ```
146    pub fn subset_eq(&self, other: &Mime) -> bool {
147        if other.basetype() != "*" && self.basetype() != other.basetype() {
148            return false;
149        }
150        if other.subtype() != "*" && self.subtype() != other.subtype() {
151            return false;
152        }
153        for (name, value) in other.params.iter() {
154            if !self.param(name.clone()).map(|v| v == value).unwrap_or(false) {
155                return false;
156            }
157        }
158        true
159    }
160}
161
162impl Display for Mime {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        parse::format(self, f)
165    }
166}
167
168// impl Debug for Mime {
169//     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170//         Debug::fmt(&self.essence, f)
171//     }
172// }
173
174impl FromStr for Mime {
175    type Err = crate::Error;
176
177    /// Create a new `Mime`.
178    ///
179    /// Follows the [WHATWG MIME parsing algorithm](https://mimesniff.spec.whatwg.org/#parsing-a-mime-type).
180    fn from_str(s: &str) -> Result<Self, Self::Err> {
181        parse::parse(s)
182    }
183}
184
185impl<'a> From<&'a str> for Mime {
186    fn from(value: &'a str) -> Self {
187        Self::from_str(value).unwrap()
188    }
189}
190
191impl ToHeaderValues for Mime {
192    type Iter = option::IntoIter<HeaderValue>;
193
194    fn to_header_values(&self) -> crate::Result<Self::Iter> {
195        let mime = self.clone();
196        let header: HeaderValue = mime.into();
197
198        // A HeaderValue will always convert into itself.
199        Ok(header.to_header_values().unwrap())
200    }
201}
202
203/// A parameter name.
204#[derive(Debug, Clone, PartialEq, Eq, Hash)]
205pub struct ParamName(Cow<'static, str>);
206
207impl ParamName {
208    /// Get the name as a `&str`
209    pub fn as_str(&self) -> &str {
210        &self.0
211    }
212}
213
214impl Display for ParamName {
215    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
216        Display::fmt(&self.0, f)
217    }
218}
219
220impl FromStr for ParamName {
221    type Err = crate::Error;
222
223    /// Create a new `HeaderName`.
224    ///
225    /// This checks it's valid ASCII, and lowercases it.
226    fn from_str(s: &str) -> Result<Self, Self::Err> {
227        crate::ensure!(s.is_ascii(), "String slice should be valid ASCII");
228        Ok(ParamName(Cow::Owned(s.to_ascii_lowercase())))
229    }
230}
231
232impl<'a> From<&'a str> for ParamName {
233    fn from(value: &'a str) -> Self {
234        Self::from_str(value).unwrap()
235    }
236}
237
238/// A parameter value.
239#[derive(Debug, Clone, PartialEq, Eq, Hash)]
240pub struct ParamValue(Cow<'static, str>);
241
242impl ParamValue {
243    /// Get the value as a `&str`
244    pub fn as_str(&self) -> &str {
245        &self.0
246    }
247}
248
249impl Display for ParamValue {
250    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
251        Display::fmt(&self.0, f)
252    }
253}
254
255impl<'a> PartialEq<&'a str> for ParamValue {
256    fn eq(&self, other: &&'a str) -> bool {
257        &self.0 == other
258    }
259}
260
261impl PartialEq<str> for ParamValue {
262    fn eq(&self, other: &str) -> bool {
263        self.0 == other
264    }
265}