1#![no_std]
21#![doc(html_root_url = "https://docs.rs/content-type/0.1.0")]
22
23extern crate alloc;
24
25use alloc::string::{String, ToString};
26use alloc::vec::Vec;
27use core::fmt;
28
29#[cfg(doctest)]
31#[doc = include_str!("../README.md")]
32struct ReadmeDoctests;
33
34#[derive(Debug, Clone, PartialEq, Eq, Default)]
39pub struct ContentType {
40 pub type_: String,
42 pub parameters: Vec<(String, String)>,
44}
45
46impl ContentType {
47 #[must_use]
49 pub fn new(type_: impl Into<String>) -> Self {
50 Self {
51 type_: type_.into(),
52 parameters: Vec::new(),
53 }
54 }
55
56 #[must_use]
58 pub fn with_parameter(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
59 self.parameters.push((name.into(), value.into()));
60 self
61 }
62
63 #[must_use]
69 pub fn get_parameter(&self, name: &str) -> Option<&str> {
70 let lname = lowercase(name);
71 self.parameters
72 .iter()
73 .find(|(k, _)| lowercase(k) == lname)
74 .map(|(_, v)| v.as_str())
75 }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq)]
80pub enum FormatError {
81 InvalidType(String),
83 InvalidParameterName(String),
85 InvalidParameterValue(String),
87}
88
89impl fmt::Display for FormatError {
90 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
91 match self {
92 FormatError::InvalidType(t) => write!(f, "invalid type: {t}"),
93 FormatError::InvalidParameterName(n) => write!(f, "invalid parameter name: {n}"),
94 FormatError::InvalidParameterValue(v) => write!(f, "invalid parameter value: {v}"),
95 }
96 }
97}
98
99impl core::error::Error for FormatError {}
100
101#[must_use]
113pub fn parse(header: &str) -> ContentType {
114 parse_with(header, true)
115}
116
117#[must_use]
122pub fn parse_with(header: &str, parse_parameters: bool) -> ContentType {
123 let chars: Vec<char> = header.chars().collect();
124 let len = chars.len();
125
126 let mut index = skip_ows(&chars, 0, len);
127 let value_start = index;
128 index = skip_value(&chars, index, len);
129 let value_end = trailing_ows(&chars, value_start, index);
130 let type_ = lowercase_slice(&chars[value_start..value_end]);
131
132 let parameters = if parse_parameters {
133 parse_parameters_impl(&chars, index, len)
134 } else {
135 Vec::new()
136 };
137
138 ContentType { type_, parameters }
139}
140
141pub fn format(content_type: &ContentType) -> Result<String, FormatError> {
156 if !is_type(&content_type.type_) {
157 return Err(FormatError::InvalidType(content_type.type_.clone()));
158 }
159 let mut result = content_type.type_.clone();
160 for (name, value) in &content_type.parameters {
161 if !is_token(name) {
162 return Err(FormatError::InvalidParameterName(name.clone()));
163 }
164 result.push_str("; ");
165 result.push_str(name);
166 result.push('=');
167 result.push_str(&qstring(value)?);
168 }
169 Ok(result)
170}
171
172const SP: char = ' ';
173const HTAB: char = '\t';
174const SEMI: char = ';';
175const EQ: char = '=';
176const DQUOTE: char = '"';
177const BSLASH: char = '\\';
178
179fn parse_parameters_impl(chars: &[char], mut index: usize, len: usize) -> Vec<(String, String)> {
180 let mut parameters: Vec<(String, String)> = Vec::new();
181
182 'parameter: while index < len {
183 index = skip_ows(chars, index + 1, len); let key_start = index;
185 while index < len {
186 let code = chars[index];
187 if code == SEMI {
188 continue 'parameter;
189 }
190 if code == EQ {
191 let key_end = trailing_ows(chars, key_start, index);
192 let key = lowercase_slice(&chars[key_start..key_end]);
193 index = skip_ows(chars, index + 1, len);
194
195 if index < len && chars[index] == DQUOTE {
196 index += 1;
197 let mut value = String::new();
198 let mut closed = false;
199 while index < len {
200 let code = chars[index];
201 index += 1;
202 if code == DQUOTE {
203 index = skip_value(chars, index, len);
204 closed = true;
205 break;
206 }
207 if code == BSLASH && index < len {
208 value.push(chars[index]);
209 index += 1;
210 continue;
211 }
212 value.push(code);
213 }
214 if closed {
216 insert_first(&mut parameters, key, value);
217 }
218 continue 'parameter;
219 }
220
221 let value_start = index;
222 index = skip_value(chars, index, len);
223 let value_end = trailing_ows(chars, value_start, index);
224 let value = chars[value_start..value_end].iter().collect();
225 insert_first(&mut parameters, key, value);
226 continue 'parameter;
227 }
228 index += 1;
229 }
230 }
231
232 parameters
233}
234
235fn insert_first(parameters: &mut Vec<(String, String)>, key: String, value: String) {
237 if !parameters.iter().any(|(k, _)| *k == key) {
238 parameters.push((key, value));
239 }
240}
241
242fn skip_value(chars: &[char], mut index: usize, len: usize) -> usize {
244 while index < len && chars[index] != SEMI {
245 index += 1;
246 }
247 index
248}
249
250fn skip_ows(chars: &[char], mut index: usize, len: usize) -> usize {
252 while index < len && (chars[index] == SP || chars[index] == HTAB) {
253 index += 1;
254 }
255 index
256}
257
258fn trailing_ows(chars: &[char], start: usize, mut end: usize) -> usize {
260 while end > start && (chars[end - 1] == SP || chars[end - 1] == HTAB) {
261 end -= 1;
262 }
263 end
264}
265
266fn lowercase(s: &str) -> String {
267 s.chars().flat_map(char::to_lowercase).collect()
268}
269
270fn lowercase_slice(chars: &[char]) -> String {
271 chars.iter().flat_map(|c| c.to_lowercase()).collect()
272}
273
274fn is_token_char(c: char) -> bool {
276 c.is_ascii_alphanumeric()
277 || matches!(
278 c,
279 '!' | '#'
280 | '$'
281 | '%'
282 | '&'
283 | '\''
284 | '*'
285 | '+'
286 | '.'
287 | '^'
288 | '_'
289 | '`'
290 | '|'
291 | '~'
292 | '-'
293 )
294}
295
296fn is_token(s: &str) -> bool {
298 !s.is_empty() && s.chars().all(is_token_char)
299}
300
301fn is_type(s: &str) -> bool {
303 match s.split_once('/') {
304 Some((ty, sub)) => is_token(ty) && is_token(sub),
305 None => false,
306 }
307}
308
309fn is_text_char(c: char) -> bool {
311 let n = c as u32;
312 n == 0x09 || (0x20..=0x7e).contains(&n) || (0x80..=0xff).contains(&n)
313}
314
315fn qstring(value: &str) -> Result<String, FormatError> {
317 if is_token(value) {
318 return Ok(value.to_string());
319 }
320 if value.chars().all(is_text_char) {
321 let mut out = String::with_capacity(value.len() + 2);
322 out.push(DQUOTE);
323 for c in value.chars() {
324 if c == BSLASH || c == DQUOTE {
325 out.push(BSLASH);
326 }
327 out.push(c);
328 }
329 out.push(DQUOTE);
330 return Ok(out);
331 }
332 Err(FormatError::InvalidParameterValue(value.to_string()))
333}