mediatype/
value.rs

1use super::parse::*;
2use std::{
3    borrow::Cow,
4    cmp::Ordering,
5    fmt,
6    hash::{Hash, Hasher},
7    iter,
8};
9
10/// A media-type parameter value.
11///
12/// The constructor accepts both an unquoted string and a quoted string like `"Hello Wold!"`.
13/// If the string is not quoted, the allowed characters are
14/// alphabets, numbers and `!#$&-^_.+%*'`.
15///
16/// ```
17/// # use mediatype::Value;
18/// let utf8 = Value::new("UTF-8").unwrap();
19/// let utf8_quoted = Value::new("\"UTF-8\"").unwrap();
20/// assert_eq!(utf8, utf8_quoted);
21/// assert_eq!(utf8_quoted.as_str(), "\"UTF-8\"");
22/// assert_eq!(utf8_quoted.unquoted_str(), "UTF-8");
23///
24/// let double_quoted = Value::new("\" \\\" \"").unwrap();
25/// assert_eq!(double_quoted.as_str(), "\" \\\" \"");
26/// assert_eq!(double_quoted.unquoted_str(), " \" ");
27/// ```
28#[derive(Debug, Copy, Clone)]
29pub struct Value<'a>(&'a str);
30
31impl<'a> Value<'a> {
32    /// Constructs a `Value`.
33    ///
34    /// If the string is not valid as a value, returns `None`.
35    #[must_use]
36    pub fn new(s: &'a str) -> Option<Self> {
37        if let Some(quoted) = s.strip_prefix('\"') {
38            if quoted.len() > 1 && parse_quoted_value(quoted) == Ok(quoted.len()) {
39                return Some(Self(s));
40            }
41        } else if is_restricted_str(s) {
42            return Some(Self(s));
43        }
44        None
45    }
46
47    /// Returns the underlying string.
48    #[must_use]
49    pub const fn as_str(&self) -> &'a str {
50        self.0
51    }
52
53    /// Returns the unquoted string.
54    #[must_use]
55    pub fn unquoted_str(&self) -> Cow<'_, str> {
56        if self.0.starts_with('"') {
57            let inner = &self.0[1..self.0.len() - 1];
58            if inner.contains('\\') {
59                let mut s = String::with_capacity(inner.len());
60                let mut quoted = false;
61                for c in inner.chars() {
62                    match c {
63                        _ if quoted => {
64                            quoted = false;
65                            s.push(c);
66                        }
67                        '\\' => {
68                            quoted = true;
69                        }
70                        _ => {
71                            s.push(c);
72                        }
73                    }
74                }
75                Cow::Owned(s)
76            } else {
77                Cow::Borrowed(inner)
78            }
79        } else {
80            Cow::Borrowed(self.0)
81        }
82    }
83
84    /// Generates a quoted string if necessary.
85    ///
86    /// ```
87    /// # use mediatype::Value;
88    /// assert_eq!(Value::quote("UTF-8"), "UTF-8");
89    /// assert_eq!(Value::quote("Hello world!"), "\"Hello world!\"");
90    /// assert_eq!(
91    ///     Value::quote(r#" "What's wrong?" "#),
92    ///     "\" \\\"What's wrong\\?\\\" \""
93    /// );
94    /// ```
95    #[must_use]
96    pub fn quote(s: &str) -> Cow<'_, str> {
97        if is_restricted_str(s) {
98            Cow::Borrowed(s)
99        } else {
100            let inner = s.chars().flat_map(|c| {
101                if is_restricted_char(c) || c == ' ' {
102                    vec![c]
103                } else {
104                    vec!['\\', c]
105                }
106            });
107            let quoted = iter::once('"').chain(inner).chain(iter::once('"'));
108            Cow::Owned(quoted.collect())
109        }
110    }
111
112    pub(crate) const fn new_unchecked(s: &'a str) -> Self {
113        Self(s)
114    }
115}
116
117impl fmt::Display for Value<'_> {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        f.write_str(self.0)
120    }
121}
122
123impl PartialEq for Value<'_> {
124    fn eq(&self, other: &Self) -> bool {
125        self.unquoted_str() == other.unquoted_str()
126    }
127}
128
129impl Eq for Value<'_> {}
130
131impl PartialOrd for Value<'_> {
132    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
133        Some(self.cmp(other))
134    }
135}
136
137impl Ord for Value<'_> {
138    fn cmp(&self, other: &Self) -> Ordering {
139        self.unquoted_str().cmp(&other.unquoted_str())
140    }
141}
142
143impl Hash for Value<'_> {
144    fn hash<H: Hasher>(&self, state: &mut H) {
145        self.unquoted_str().hash(state);
146    }
147}
148
149impl PartialEq<String> for Value<'_> {
150    fn eq(&self, other: &String) -> bool {
151        self.eq(other.as_str())
152    }
153}
154
155impl PartialOrd<String> for Value<'_> {
156    fn partial_cmp(&self, other: &String) -> Option<Ordering> {
157        self.partial_cmp(other.as_str())
158    }
159}
160
161impl PartialEq<&String> for Value<'_> {
162    fn eq(&self, other: &&String) -> bool {
163        self.eq(other.as_str())
164    }
165}
166
167impl PartialOrd<&String> for Value<'_> {
168    fn partial_cmp(&self, other: &&String) -> Option<Ordering> {
169        self.partial_cmp(other.as_str())
170    }
171}
172
173impl PartialEq<str> for Value<'_> {
174    fn eq(&self, other: &str) -> bool {
175        self.unquoted_str() == other
176    }
177}
178
179impl PartialOrd<str> for Value<'_> {
180    fn partial_cmp(&self, other: &str) -> Option<Ordering> {
181        Some(self.unquoted_str().as_ref().cmp(other))
182    }
183}
184
185impl PartialEq<&str> for Value<'_> {
186    fn eq(&self, other: &&str) -> bool {
187        self.eq(*other)
188    }
189}
190
191impl PartialOrd<&str> for Value<'_> {
192    fn partial_cmp(&self, other: &&str) -> Option<Ordering> {
193        self.partial_cmp(*other)
194    }
195}
196
197impl PartialEq<Cow<'_, str>> for Value<'_> {
198    fn eq(&self, other: &Cow<'_, str>) -> bool {
199        self.eq(other.as_ref())
200    }
201}
202
203impl PartialOrd<Cow<'_, str>> for Value<'_> {
204    fn partial_cmp(&self, other: &Cow<'_, str>) -> Option<Ordering> {
205        self.partial_cmp(other.as_ref())
206    }
207}
208
209impl PartialEq<&Cow<'_, str>> for Value<'_> {
210    fn eq(&self, other: &&Cow<'_, str>) -> bool {
211        self.eq(other.as_ref())
212    }
213}
214
215impl PartialOrd<&Cow<'_, str>> for Value<'_> {
216    fn partial_cmp(&self, other: &&Cow<'_, str>) -> Option<Ordering> {
217        self.partial_cmp(other.as_ref())
218    }
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn unquoted_str() {
227        assert_eq!(Value::new("\"\\a\\\\\"").unwrap().unquoted_str(), "a\\");
228        assert_eq!(Value::new("\"\\\"\"").unwrap().unquoted_str(), "\"");
229        assert_eq!(Value::new("\"\\a\\b\\c\"").unwrap().unquoted_str(), "abc");
230        assert_eq!(
231            Value::new("\" \\\"What's wrong\\?\\\" \"")
232                .unwrap()
233                .unquoted_str(),
234            r#" "What's wrong?" "#
235        );
236    }
237}