1use crate::{
27 class,
28 error::{ErrorKind, ParseError, Result, WriteError, err},
29 limits::Limits,
30 param::{self, Params},
31 scan::control_kind,
32 text::{Word, is_token, skip_ows, trim_ows},
33 write::{Sink, reserve, word_len, write_word},
34};
35
36pub fn parse_media_type(value: &[u8]) -> Result<MediaType<'_>> {
51 parse(value, &Limits::DEFAULT)
52}
53
54fn bad_byte(value: &[u8], at: usize) -> ParseError {
56 match value.get(at) {
57 Some(&b) if b < 0x20 || b == 0x7f => ParseError::new(control_kind(b), at),
58 _ => ParseError::new(ErrorKind::InvalidMediaType, at),
59 }
60}
61
62fn essence(value: &[u8], base: usize) -> Result<(&[u8], &[u8], usize)> {
65 let type_len = value.iter().take_while(|&&b| class::is_tchar(b)).count();
66 if type_len == 0 {
67 return Err(bad_byte(value, 0).shifted(base));
68 }
69 if value.get(type_len) != Some(&b'/') {
70 return Err(bad_byte(value, type_len).shifted(base));
71 }
72 let sub_start = type_len + 1;
73 let sub_len = value
74 .get(sub_start..)
75 .unwrap_or(&[])
76 .iter()
77 .take_while(|&&b| class::is_tchar(b))
78 .count();
79 if sub_len == 0 {
80 return Err(bad_byte(value, sub_start).shifted(base));
81 }
82 let type_ = value.get(..type_len).unwrap_or(&[]);
83 let subtype = value.get(sub_start..sub_start + sub_len).unwrap_or(&[]);
84 Ok((type_, subtype, sub_start + sub_len))
85}
86
87pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<MediaType<'a>> {
112 let lead = skip_ows(value);
113 let trimmed = trim_ows(value);
114 let (type_, subtype, used) = essence(trimmed, lead)?;
115 if type_ == b"*" {
116 return err(ErrorKind::Wildcard, lead);
117 }
118 if subtype == b"*" {
119 return err(ErrorKind::Wildcard, lead + type_.len() + 1);
120 }
121 let rest = trimmed.get(used..).unwrap_or(&[]);
122 let (params, n) = param::parse(rest, limits).map_err(|e| e.shifted(lead + used))?;
123 let end = used + n;
124 if end < trimmed.len() {
125 let at = end + skip_ows(trimmed.get(end..).unwrap_or(&[]));
126 return Err(bad_byte(trimmed, at).shifted(lead));
127 }
128 Ok(MediaType {
129 type_,
130 subtype,
131 params,
132 })
133}
134
135pub fn write(
157 type_: &str,
158 subtype: &str,
159 params: &[(&str, &str)],
160 out: &mut impl Sink,
161) -> core::result::Result<(), WriteError> {
162 let (t, s) = (type_.as_bytes(), subtype.as_bytes());
163 if !is_token(t) || !is_token(s) || t == b"*" || s == b"*" {
164 return Err(WriteError::NotToken);
165 }
166 let mut len = t.len().saturating_add(1).saturating_add(s.len());
167 for (name, value) in params {
168 if !is_token(name.as_bytes()) {
169 return Err(WriteError::NotToken);
170 }
171 let v = word_len(value.as_bytes())?;
172 len = len
173 .saturating_add(3)
174 .saturating_add(name.len())
175 .saturating_add(v);
176 }
177 reserve(out, len)?;
178 out.put(t)?;
179 out.put(b"/")?;
180 out.put(s)?;
181 for (name, value) in params {
182 out.put(b"; ")?;
183 out.put(name.as_bytes())?;
184 out.put(b"=")?;
185 write_word(value.as_bytes(), out)?;
186 }
187 Ok(())
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
201pub struct MediaType<'a> {
202 type_: &'a [u8],
203 subtype: &'a [u8],
204 params: Params<'a>,
205}
206
207impl<'a> MediaType<'a> {
208 #[must_use]
210 pub const fn type_(&self) -> &'a [u8] {
211 self.type_
212 }
213
214 #[must_use]
216 pub const fn subtype(&self) -> &'a [u8] {
217 self.subtype
218 }
219
220 #[must_use]
223 pub fn suffix(&self) -> Option<&'a [u8]> {
224 let plus = self.subtype.iter().rposition(|&b| b == b'+')?;
225 self.subtype.get(plus + 1..).filter(|s| !s.is_empty())
226 }
227
228 #[must_use]
230 pub fn essence_is(&self, essence: &str) -> bool {
231 match essence.split_once('/') {
232 Some((t, s)) => self.is(t, s),
233 None => false,
234 }
235 }
236
237 #[must_use]
240 pub fn is(&self, type_: &str, subtype: &str) -> bool {
241 self.type_.eq_ignore_ascii_case(type_.as_bytes())
242 && self.subtype.eq_ignore_ascii_case(subtype.as_bytes())
243 }
244
245 #[must_use]
247 pub const fn params(&self) -> Params<'a> {
248 self.params
249 }
250
251 #[must_use]
253 pub fn param(&self, name: &str) -> Option<Word<'a>> {
254 self.params.get(name)
255 }
256
257 #[must_use]
259 pub fn charset(&self) -> Option<Word<'a>> {
260 self.params.get("charset")
261 }
262
263 pub fn boundary(&self) -> Result<Option<Boundary<'a>>> {
271 let Some(word) = self.params.get("boundary") else {
272 return Ok(None);
273 };
274 let bytes = match word {
275 Word::Token(t) => t,
276 Word::Quoted(q) if q.has_escapes() => {
278 let at = q.raw().iter().position(|&b| b == b'\\').unwrap_or(0);
279 return err(ErrorKind::InvalidBoundary, at);
280 }
281 Word::Quoted(q) => q.raw(),
282 };
283 if bytes.is_empty() {
284 return err(ErrorKind::InvalidBoundary, 0);
285 }
286 if let Some(at) = bytes
287 .iter()
288 .position(|&b| !(class::is_bchar_nospace(b) || b == b' '))
289 {
290 return err(ErrorKind::InvalidBoundary, at);
291 }
292 if bytes.len() > 70 {
293 return err(ErrorKind::InvalidBoundary, 70);
294 }
295 if bytes.last() == Some(&b' ') {
296 return err(ErrorKind::InvalidBoundary, bytes.len() - 1);
297 }
298 Ok(Some(Boundary(bytes)))
299 }
300
301 #[must_use]
303 pub fn is_json(&self) -> bool {
304 self.is("application", "json")
305 || self
306 .suffix()
307 .is_some_and(|s| s.eq_ignore_ascii_case(b"json"))
308 }
309
310 #[must_use]
312 pub fn is_form_urlencoded(&self) -> bool {
313 self.is("application", "x-www-form-urlencoded")
314 }
315
316 #[must_use]
318 pub fn is_multipart_form_data(&self) -> bool {
319 self.is("multipart", "form-data")
320 }
321
322 pub fn write_canonical(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
343 let mut len = self.type_.len() + 1 + self.subtype.len();
344 for p in self.params.iter() {
345 len = len.saturating_add(3 + p.name.len() + raw_word_len(&p.value));
346 }
347 reserve(out, len)?;
348 put_lower(self.type_, out)?;
349 out.put(b"/")?;
350 put_lower(self.subtype, out)?;
351 for p in self.params.iter() {
352 out.put(b"; ")?;
353 put_lower(p.name, out)?;
354 out.put(b"=")?;
355 match p.value {
356 Word::Token(t) => out.put(t)?,
357 Word::Quoted(q) => {
358 out.put(b"\"")?;
359 out.put(q.raw())?;
360 out.put(b"\"")?;
361 }
362 }
363 }
364 Ok(())
365 }
366}
367
368fn raw_word_len(w: &Word<'_>) -> usize {
370 match w {
371 Word::Token(t) => t.len(),
372 Word::Quoted(q) => q.raw().len() + 2,
373 }
374}
375
376fn put_lower(s: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
378 let mut buf = [0u8; 64];
379 for chunk in s.chunks(buf.len()) {
380 let dst = buf.get_mut(..chunk.len()).unwrap_or(&mut []);
381 for (d, &c) in dst.iter_mut().zip(chunk) {
382 *d = c.to_ascii_lowercase();
383 }
384 out.put(dst)?;
385 }
386 Ok(())
387}
388
389#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
399pub struct Boundary<'a>(&'a [u8]);
400
401impl<'a> Boundary<'a> {
402 #[must_use]
404 pub const fn as_bytes(&self) -> &'a [u8] {
405 self.0
406 }
407}
408
409#[cfg(test)]
410mod tests {
411 use std::{vec, vec::Vec};
412
413 use super::*;
414 use crate::write::SliceSink;
415
416 fn mt(s: &[u8]) -> Result<MediaType<'_>> {
417 parse_media_type(s)
418 }
419
420 #[test]
421 fn test_media_type_edge_cases() {
422 assert!(mt(b"text/html").unwrap().is("text", "html"));
423 assert!(mt(b"TEXT/HTML").unwrap().essence_is("text/html"));
424 assert!(mt(b"text/html;").unwrap().params().is_empty());
425 assert!(mt(b"text/html ;charset=utf-8").unwrap().charset().is_some());
426 assert!(mt(b" text/html \t").is_ok());
427 let e = mt(b"text/ html").unwrap_err();
428 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 5));
429 let e = mt(b"text /html").unwrap_err();
430 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 4));
431 assert_eq!(
432 mt(b"text/").unwrap_err().kind(),
433 ErrorKind::InvalidMediaType
434 );
435 assert_eq!(
436 mt(b"/html").unwrap_err().kind(),
437 ErrorKind::InvalidMediaType
438 );
439 assert_eq!(mt(b"").unwrap_err().kind(), ErrorKind::InvalidMediaType);
440 assert_eq!(mt(b"text").unwrap_err().kind(), ErrorKind::InvalidMediaType);
441 assert_eq!(mt(b"*/*").unwrap_err().kind(), ErrorKind::Wildcard);
442 let e = mt(b"text/*").unwrap_err();
443 assert_eq!((e.kind(), e.offset()), (ErrorKind::Wildcard, 5));
444 assert_eq!(
445 mt(b"text/html x").unwrap_err().kind(),
446 ErrorKind::InvalidMediaType
447 );
448 assert_eq!(mt(b"text/html\r").unwrap_err().kind(), ErrorKind::BareCr);
449 assert_eq!(
450 mt(b"text/html;a=b\nc").unwrap_err().kind(),
451 ErrorKind::LineFeed
452 );
453 let e = mt(b"text/html; charset=a; Charset=b").unwrap_err();
454 assert_eq!((e.kind(), e.offset()), (ErrorKind::DuplicateParameter, 22));
455 }
456
457 #[test]
458 fn test_media_type_suffix_and_predicates() {
459 let v = mt(b"application/vnd.api+json").unwrap();
460 assert_eq!(v.suffix(), Some(&b"json"[..]));
461 assert!(v.is_json());
462 assert!(mt(b"application/json").unwrap().is_json());
463 assert!(!mt(b"application/jsonx").unwrap().is_json());
464 assert_eq!(mt(b"application/a+").unwrap().suffix(), None);
465 assert!(
466 mt(b"application/x-www-form-urlencoded")
467 .unwrap()
468 .is_form_urlencoded()
469 );
470 assert!(!mt(b"text/html").unwrap().essence_is("texthtml"));
471 }
472
473 fn boundary(s: &[u8]) -> Result<Option<Vec<u8>>> {
474 Ok(mt(s)?.boundary()?.map(|b| b.as_bytes().to_vec()))
475 }
476
477 #[test]
478 fn test_boundary_rfc2046_rules() {
479 assert_eq!(boundary(b"multipart/form-data").unwrap(), None);
480 assert_eq!(
481 boundary(b"multipart/form-data; boundary=")
482 .unwrap_err()
483 .kind(),
484 ErrorKind::MissingValue
485 );
486 assert_eq!(
487 boundary(b"multipart/form-data; boundary=\"\"")
488 .unwrap_err()
489 .kind(),
490 ErrorKind::InvalidBoundary
491 );
492 let seventy = vec![b'a'; 70];
493 let mut v = b"multipart/mixed; boundary=".to_vec();
494 v.extend_from_slice(&seventy);
495 assert_eq!(boundary(&v).unwrap(), Some(seventy));
496 v.push(b'a');
497 assert_eq!(boundary(&v).unwrap_err().kind(), ErrorKind::InvalidBoundary);
498 assert_eq!(
499 boundary(b"multipart/mixed; boundary=\"ab \"")
500 .unwrap_err()
501 .kind(),
502 ErrorKind::InvalidBoundary
503 );
504 assert_eq!(
505 boundary(b"multipart/mixed; boundary=\"a b\"").unwrap(),
506 Some(b"a b".to_vec())
507 );
508 assert_eq!(
509 boundary(b"multipart/mixed; boundary=\"a\\\\b\"")
510 .unwrap_err()
511 .kind(),
512 ErrorKind::InvalidBoundary
513 );
514 assert_eq!(
515 boundary(b"multipart/mixed; boundary=a*b")
516 .unwrap_err()
517 .kind(),
518 ErrorKind::InvalidBoundary
519 );
520 assert_eq!(
521 mt(b"multipart/mixed; boundary=a; boundary=b")
522 .unwrap_err()
523 .kind(),
524 ErrorKind::DuplicateParameter
525 );
526 }
527
528 #[test]
529 fn test_media_write_refuses_bad_names_and_round_trips() {
530 let mut buf = [0u8; 128];
531 let mut out = SliceSink::new(&mut buf);
532 assert_eq!(write("text", "*", &[], &mut out), Err(WriteError::NotToken));
533 assert_eq!(
534 write("te xt", "a", &[], &mut out),
535 Err(WriteError::NotToken)
536 );
537 assert_eq!(
538 write("a", "b", &[("x y", "1")], &mut out),
539 Err(WriteError::NotToken)
540 );
541 assert_eq!(
542 write("a", "b", &[("x", "1\r\n")], &mut out),
543 Err(WriteError::Forbidden { at: 1 })
544 );
545 assert!(out.is_empty());
546 write("a", "b", &[("x", "1 2"), ("y", "")], &mut out).unwrap();
547 let back = mt(out.written()).unwrap();
548 assert!(back.param("x").unwrap().eq_bytes(b"1 2"));
549 assert!(back.param("y").unwrap().is_empty());
550 }
551
552 #[test]
553 fn test_write_canonical_lowercases_names_only() {
554 let v = mt(b"TEXT/Plain; FORMAT=Flowed;x=\"A\\\"B\"").unwrap();
555 let mut buf = [0u8; 64];
556 let mut out = SliceSink::new(&mut buf);
557 v.write_canonical(&mut out).unwrap();
558 assert_eq!(out.written(), b"text/plain; format=Flowed; x=\"A\\\"B\"");
559 let mut small = [0u8; 4];
560 let mut out = SliceSink::new(&mut small);
561 assert_eq!(v.write_canonical(&mut out), Err(WriteError::Full));
562 assert!(out.is_empty());
563 }
564}