parse_net/media.rs
1//! Media types: `type "/" subtype parameters` (RFC 9110 section 8.3.1), and
2//! the RFC 2046 multipart boundary.
3//!
4//! Type, subtype, and parameter names are case-insensitive; parameter values
5//! are case-sensitive unless the parameter's definition says otherwise
6//! (`charset` is case-insensitive, RFC 9110 section 8.3.2).
7//!
8//! # Examples
9//!
10//! ```
11//! use parse_net as pn;
12//!
13//! let mt = pn::parse_media_type(b"Multipart/Form-Data; boundary=\"----x y\"")?;
14//! assert!(mt.is_multipart_form_data() && mt.is("multipart", "form-data"));
15//! assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"----x y"[..]));
16//!
17//! assert_eq!(pn::parse_media_type(b"text/*").unwrap_err().kind(), pn::ErrorKind::Wildcard);
18//!
19//! let mut buf = [0u8; 64];
20//! let mut out = pn::SliceSink::new(&mut buf);
21//! pn::media::write("text", "plain", &[("charset", "utf-8")], &mut out)?;
22//! assert_eq!(out.written(), b"text/plain; charset=utf-8");
23//! # Ok::<(), Box<dyn std::error::Error>>(())
24//! ```
25
26use crate::{
27 class,
28 error::{ErrorKind, ParseError, Result, WriteError, err},
29 limits::Limits,
30 negotiate::QValue,
31 param::{self, Params},
32 scan::control_kind,
33 text::{Word, is_token, skip_ows, trim_ows},
34 write::{Sink, reserve, unique_names, word_len, write_word},
35};
36
37/// Parses a `Content-Type` style media type with the default [`Limits`].
38///
39/// # Errors
40///
41/// As [`parse`].
42///
43/// # Examples
44///
45/// ```
46/// let mt = parse_net::parse_media_type(b"text/html; charset=UTF-8")?;
47/// assert!(mt.essence_is("text/html"));
48/// assert!(mt.charset().is_some_and(|c| c.eq_ignore_ascii_case(b"utf-8")));
49/// # Ok::<(), parse_net::ParseError>(())
50/// ```
51pub fn parse_media_type(value: &[u8]) -> Result<MediaType<'_>> {
52 parse(value, &Limits::DEFAULT)
53}
54
55/// The error for a byte that ends a media type where it may not.
56fn bad_byte(value: &[u8], at: usize) -> ParseError {
57 match value.get(at) {
58 Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
59 _ => ParseError::new(ErrorKind::InvalidMediaType, at),
60 }
61}
62
63/// Parses `type "/" subtype` at the start of `value` (OWS already skipped);
64/// returns both and the bytes used.
65fn essence(value: &[u8], base: usize) -> Result<(&[u8], &[u8], usize)> {
66 let type_len = value.iter().take_while(|&&b| class::is_tchar(b)).count();
67 if type_len == 0 {
68 return Err(bad_byte(value, 0).shifted(base));
69 }
70 if value.get(type_len) != Some(&b'/') {
71 return Err(bad_byte(value, type_len).shifted(base));
72 }
73 let sub_start = type_len + 1;
74 let sub_len = value
75 .get(sub_start..)
76 .unwrap_or(&[])
77 .iter()
78 .take_while(|&&b| class::is_tchar(b))
79 .count();
80 if sub_len == 0 {
81 return Err(bad_byte(value, sub_start).shifted(base));
82 }
83 let type_ = value.get(..type_len).unwrap_or(&[]);
84 let subtype = value.get(sub_start..sub_start + sub_len).unwrap_or(&[]);
85 Ok((type_, subtype, sub_start + sub_len))
86}
87
88/// Parses a media type, as in `Content-Type`.
89///
90/// Type and subtype are tokens around `/`; OWS is allowed only before `;` and
91/// around the whole value; parameters follow the RFC 9110 grammar with the
92/// duplicate policy of `limits` (refusal by default, so `charset` or
93/// `boundary` given twice is refused).
94///
95/// # Errors
96///
97/// [`ErrorKind::InvalidMediaType`] for a missing `/`, an empty or non-token
98/// type or subtype, whitespace inside the essence, or bytes after the
99/// parameters; [`ErrorKind::Wildcard`] for `*` as type or subtype; a control
100/// kind for a control byte; the parameter errors of
101/// [`param::parse`].
102///
103/// # Examples
104///
105/// ```
106/// use parse_net::{media, ErrorKind, Limits};
107///
108/// let err = media::parse(b"text/ html", &Limits::DEFAULT).unwrap_err();
109/// assert_eq!((err.kind(), err.offset()), (ErrorKind::InvalidMediaType, 5));
110/// assert_eq!(err.to_string(), "invalid media type at offset 5");
111/// ```
112pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<MediaType<'a>> {
113 let lead = skip_ows(value);
114 let trimmed = trim_ows(value);
115 let (type_, subtype, used) = essence(trimmed, lead)?;
116 if type_ == b"*" {
117 return err(ErrorKind::Wildcard, lead);
118 }
119 if subtype == b"*" {
120 return err(ErrorKind::Wildcard, lead + type_.len() + 1);
121 }
122 let rest = trimmed.get(used..).unwrap_or(&[]);
123 let (params, n) = param::parse(rest, limits).map_err(|e| e.shifted(lead + used))?;
124 let end = used + n;
125 if end < trimmed.len() {
126 let at = end + skip_ows(trimmed.get(end..).unwrap_or(&[]));
127 return Err(bad_byte(trimmed, at).shifted(lead));
128 }
129 Ok(MediaType {
130 type_,
131 subtype,
132 params,
133 })
134}
135
136/// Parses a media range, as in one member of `Accept`: `*/*`, `type/*`, or a
137/// media type, with an optional weight.
138///
139/// The first parameter named `q` (ASCII case-insensitive) is the weight; it
140/// and every parameter after it are `accept-ext` and take no part in
141/// matching (RFC 9110 section 12.5.1). A range without `q` has weight 1.
142///
143/// # Errors
144///
145/// [`ErrorKind::Wildcard`] for `*/subtype`; [`ErrorKind::InvalidQValue`];
146/// the errors of [`parse`].
147///
148/// # Examples
149///
150/// ```
151/// use parse_net::{media, Limits};
152///
153/// let r = media::parse_range(b"text/*;q=0.5", &Limits::DEFAULT)?;
154/// assert_eq!((r.specificity(), r.weight().thousandths()), (2, 500));
155/// let html = parse_net::parse_media_type(b"text/html")?;
156/// assert!(r.matches(&html));
157/// assert!(media::parse_range(b"*/html", &Limits::DEFAULT).is_err());
158/// # Ok::<(), parse_net::ParseError>(())
159/// ```
160pub fn parse_range<'a>(value: &'a [u8], limits: &Limits) -> Result<MediaRange<'a>> {
161 let lead = skip_ows(value);
162 let trimmed = trim_ows(value);
163 let (type_, subtype, used) = essence(trimmed, lead)?;
164 if type_ == b"*" && subtype != b"*" {
165 return err(ErrorKind::Wildcard, lead + type_.len() + 1);
166 }
167 let rest = trimmed.get(used..).unwrap_or(&[]);
168 let (all, n) = param::parse(rest, limits).map_err(|e| e.shifted(lead + used))?;
169 let end = used + n;
170 if end < trimmed.len() {
171 let at = end + skip_ows(trimmed.get(end..).unwrap_or(&[]));
172 return Err(bad_byte(trimmed, at).shifted(lead));
173 }
174 let (params, q) = all.split_at_name(b"q");
175 let weight = match q {
176 None => QValue::ONE,
177 Some(p) => match p.value {
178 Word::Token(t) => QValue::parse(t).map_err(|e| {
179 let at = (t.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
180 ParseError::new(e.kind(), at + e.offset())
181 })?,
182 Word::Quoted(_) => {
183 let at = (p.name.as_ptr() as usize).saturating_sub(value.as_ptr() as usize);
184 return err(ErrorKind::InvalidQValue, at);
185 }
186 },
187 };
188 Ok(MediaRange {
189 type_,
190 subtype,
191 params,
192 weight,
193 })
194}
195
196/// A validated media range from `Accept`.
197///
198/// # Examples
199///
200/// ```
201/// use parse_net::{media, Limits};
202///
203/// let r = media::parse_range(b"text/html;level=1;q=0.7;ext=x", &Limits::DEFAULT)?;
204/// assert_eq!(r.params().len(), 1); // `level` only: `q` ends the media-type parameters
205/// assert_eq!(r.weight().thousandths(), 700);
206/// assert_eq!(r.specificity(), 4);
207/// # Ok::<(), parse_net::ParseError>(())
208/// ```
209#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
210pub struct MediaRange<'a> {
211 type_: &'a [u8],
212 subtype: &'a [u8],
213 params: Params<'a>,
214 weight: QValue,
215}
216
217impl<'a> MediaRange<'a> {
218 /// The type, or `*`.
219 ///
220 /// # Examples
221 ///
222 /// ```
223 /// use parse_net::{media, Limits};
224 ///
225 /// let r = media::parse_range(b"text/*", &Limits::DEFAULT)?;
226 /// assert_eq!(r.type_(), b"text");
227 /// # Ok::<(), parse_net::ParseError>(())
228 /// ```
229 #[must_use]
230 pub const fn type_(&self) -> &'a [u8] {
231 self.type_
232 }
233
234 /// The subtype, or `*`.
235 ///
236 /// # Examples
237 ///
238 /// ```
239 /// use parse_net::{media, Limits};
240 ///
241 /// assert_eq!(media::parse_range(b"text/*", &Limits::DEFAULT)?.subtype(), b"*");
242 /// assert_eq!(media::parse_range(b"text/csv", &Limits::DEFAULT)?.subtype(), b"csv");
243 /// # Ok::<(), parse_net::ParseError>(())
244 /// ```
245 #[must_use]
246 pub const fn subtype(&self) -> &'a [u8] {
247 self.subtype
248 }
249
250 /// Whether this range matches `offer`: the type and subtype match (or
251 /// are `*`), and every parameter of the range is present in the offer
252 /// with an equal value (case-sensitive, except `charset`).
253 ///
254 /// # Examples
255 ///
256 /// ```
257 /// use parse_net::{media, parse_media_type, Limits};
258 ///
259 /// let r = media::parse_range(b"text/html;charset=utf-8", &Limits::DEFAULT)?;
260 /// assert!(r.matches(&parse_media_type(b"text/html; charset=UTF-8")?));
261 /// assert!(!r.matches(&parse_media_type(b"text/html")?));
262 /// # Ok::<(), parse_net::ParseError>(())
263 /// ```
264 #[must_use]
265 pub fn matches(&self, offer: &MediaType<'_>) -> bool {
266 // `*/subtype` is refused at parse, so a `*` type means `*/*`.
267 // An exact compare first: senders and offers are usually both lowercase.
268 let same = |a: &[u8], b: &[u8]| a == b || a.eq_ignore_ascii_case(b);
269 if self.type_ != b"*" {
270 if !same(self.type_, offer.type_()) {
271 return false;
272 }
273 if self.subtype != b"*" && !same(self.subtype, offer.subtype()) {
274 return false;
275 }
276 }
277 self.params.is_empty()
278 || self.params.iter().all(|p| {
279 let charset = p.name.eq_ignore_ascii_case(b"charset");
280 offer.params().iter().any(|o| {
281 o.name.eq_ignore_ascii_case(p.name) && p.value.same_value(&o.value, charset)
282 })
283 })
284 }
285
286 /// How specific the range is: 4 for a type with parameters, 3 for an
287 /// exact type, 2 for `type/*`, 1 for `*/*`.
288 ///
289 /// # Examples
290 ///
291 /// ```
292 /// use parse_net::{media, Limits};
293 ///
294 /// let s = |v: &[u8]| media::parse_range(v, &Limits::DEFAULT).map(|r| r.specificity());
295 /// assert_eq!((s(b"*/*")?, s(b"image/*")?, s(b"image/png")?, s(b"image/png;x=1")?), (1, 2, 3, 4));
296 /// # Ok::<(), parse_net::ParseError>(())
297 /// ```
298 #[must_use]
299 pub const fn specificity(&self) -> u8 {
300 if matches!(self.type_, [b'*']) {
301 1
302 } else if matches!(self.subtype, [b'*']) {
303 2
304 } else if self.params.is_empty() {
305 3
306 } else {
307 4
308 }
309 }
310
311 /// The weight (`q`), 1 when absent.
312 ///
313 /// # Examples
314 ///
315 /// ```
316 /// use parse_net::{media, Limits, QValue};
317 ///
318 /// assert_eq!(media::parse_range(b"text/html;q=0.25", &Limits::DEFAULT)?.weight().thousandths(), 250);
319 /// assert_eq!(media::parse_range(b"text/html", &Limits::DEFAULT)?.weight(), QValue::ONE);
320 /// # Ok::<(), parse_net::ParseError>(())
321 /// ```
322 #[must_use]
323 pub const fn weight(&self) -> QValue {
324 self.weight
325 }
326
327 /// The media-type parameters: those before `q`.
328 ///
329 /// # Examples
330 ///
331 /// ```
332 /// use parse_net::{media, Limits};
333 ///
334 /// let r = media::parse_range(b"text/html;level=1;q=0.5", &Limits::DEFAULT)?;
335 /// assert_eq!(r.params().get("level").and_then(|w| w.as_str()), Some("1"));
336 /// assert!(r.params().get("q").is_none());
337 /// # Ok::<(), parse_net::ParseError>(())
338 /// ```
339 #[must_use]
340 pub const fn params(&self) -> Params<'a> {
341 self.params
342 }
343}
344
345/// Writes `type/subtype` and then `; name=value` for each pair, values as a
346/// token when they are one and as a quoted string otherwise.
347///
348/// # Errors
349///
350/// [`WriteError::NotToken`] for a type, subtype, or name that is not a token
351/// (or is `*`); [`WriteError::DuplicateName`] for two parameters with the
352/// same name (ASCII case-insensitive), which parsers refuse;
353/// [`WriteError::Forbidden`] for a control byte in a value;
354/// [`WriteError::Full`]. Nothing is written on error when the sink reports
355/// its room.
356///
357/// # Examples
358///
359/// ```
360/// use parse_net::{media, SliceSink};
361///
362/// let mut buf = [0u8; 64];
363/// let mut out = SliceSink::new(&mut buf);
364/// media::write("multipart", "form-data", &[("boundary", "a b")], &mut out)?;
365/// assert_eq!(out.written(), b"multipart/form-data; boundary=\"a b\"");
366/// # Ok::<(), parse_net::WriteError>(())
367/// ```
368pub fn write(
369 type_: &str,
370 subtype: &str,
371 params: &[(&str, &str)],
372 out: &mut impl Sink,
373) -> core::result::Result<(), WriteError> {
374 let (t, s) = (type_.as_bytes(), subtype.as_bytes());
375 if !is_token(t) || !is_token(s) || t == b"*" || s == b"*" {
376 return Err(WriteError::NotToken);
377 }
378 unique_names(params)?;
379 let mut len = t.len().saturating_add(1).saturating_add(s.len());
380 for (name, value) in params {
381 if !is_token(name.as_bytes()) {
382 return Err(WriteError::NotToken);
383 }
384 let v = word_len(value.as_bytes())?;
385 len = len
386 .saturating_add(3)
387 .saturating_add(name.len())
388 .saturating_add(v);
389 }
390 reserve(out, len)?;
391 out.put(t)?;
392 out.put(b"/")?;
393 out.put(s)?;
394 for (name, value) in params {
395 out.put(b"; ")?;
396 out.put(name.as_bytes())?;
397 out.put(b"=")?;
398 write_word(value.as_bytes(), out)?;
399 }
400 Ok(())
401}
402
403/// A validated media type.
404///
405/// # Examples
406///
407/// ```
408/// let ld = parse_net::parse_media_type(b"application/ld+json")?;
409/// assert_eq!(ld.suffix(), Some(&b"json"[..]));
410/// assert!(ld.is_json());
411/// # Ok::<(), parse_net::ParseError>(())
412/// ```
413#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
414pub struct MediaType<'a> {
415 pub(crate) type_: &'a [u8],
416 pub(crate) subtype: &'a [u8],
417 pub(crate) params: Params<'a>,
418}
419
420impl<'a> MediaType<'a> {
421 /// The type, as sent.
422 ///
423 /// # Examples
424 ///
425 /// ```
426 /// let mt = parse_net::parse_media_type(b"Text/HTML")?;
427 /// assert_eq!(mt.type_(), b"Text");
428 /// # Ok::<(), parse_net::ParseError>(())
429 /// ```
430 #[must_use]
431 pub const fn type_(&self) -> &'a [u8] {
432 self.type_
433 }
434
435 /// The subtype, as sent.
436 ///
437 /// # Examples
438 ///
439 /// ```
440 /// let mt = parse_net::parse_media_type(b"Text/HTML; charset=utf-8")?;
441 /// assert_eq!(mt.subtype(), b"HTML");
442 /// # Ok::<(), parse_net::ParseError>(())
443 /// ```
444 #[must_use]
445 pub const fn subtype(&self) -> &'a [u8] {
446 self.subtype
447 }
448
449 /// The structured syntax suffix: the part of the subtype after its last
450 /// `+` (`json` of `ld+json`), when there is one.
451 ///
452 /// # Examples
453 ///
454 /// ```
455 /// use parse_net::parse_media_type;
456 ///
457 /// assert_eq!(parse_media_type(b"image/svg+xml")?.suffix(), Some(&b"xml"[..]));
458 /// assert_eq!(parse_media_type(b"text/plain")?.suffix(), None);
459 /// # Ok::<(), parse_net::ParseError>(())
460 /// ```
461 #[must_use]
462 pub fn suffix(&self) -> Option<&'a [u8]> {
463 let plus = self.subtype.iter().rposition(|&b| b == b'+')?;
464 self.subtype.get(plus + 1..).filter(|s| !s.is_empty())
465 }
466
467 /// Whether `type/subtype` equals `essence`, ignoring ASCII case.
468 ///
469 /// # Examples
470 ///
471 /// ```
472 /// let mt = parse_net::parse_media_type(b"Application/JSON; charset=utf-8")?;
473 /// assert!(mt.essence_is("application/json"));
474 /// assert!(!mt.essence_is("application"));
475 /// # Ok::<(), parse_net::ParseError>(())
476 /// ```
477 #[must_use]
478 pub fn essence_is(&self, essence: &str) -> bool {
479 match essence.split_once('/') {
480 Some((t, s)) => self.is(t, s),
481 None => false,
482 }
483 }
484
485 /// Whether the type and subtype equal `type_` and `subtype`, ignoring
486 /// ASCII case.
487 ///
488 /// # Examples
489 ///
490 /// ```
491 /// let mt = parse_net::parse_media_type(b"TEXT/html")?;
492 /// assert!(mt.is("text", "HTML") && !mt.is("text", "plain"));
493 /// # Ok::<(), parse_net::ParseError>(())
494 /// ```
495 #[must_use]
496 pub fn is(&self, type_: &str, subtype: &str) -> bool {
497 self.type_.eq_ignore_ascii_case(type_.as_bytes())
498 && self.subtype.eq_ignore_ascii_case(subtype.as_bytes())
499 }
500
501 /// The parameters.
502 ///
503 /// # Examples
504 ///
505 /// ```
506 /// let mt = parse_net::parse_media_type(b"text/plain; charset=utf-8; format=flowed")?;
507 /// assert_eq!(mt.params().len(), 2);
508 /// # Ok::<(), parse_net::ParseError>(())
509 /// ```
510 #[must_use]
511 pub const fn params(&self) -> Params<'a> {
512 self.params
513 }
514
515 /// The value of the parameter `name` (ASCII case-insensitive).
516 ///
517 /// # Examples
518 ///
519 /// ```
520 /// let mt = parse_net::parse_media_type(b"text/plain; format=flowed")?;
521 /// assert_eq!(mt.param("Format").and_then(|w| w.as_str()), Some("flowed"));
522 /// assert!(mt.param("delsp").is_none());
523 /// # Ok::<(), parse_net::ParseError>(())
524 /// ```
525 #[must_use]
526 pub fn param(&self, name: &str) -> Option<Word<'a>> {
527 self.params.get(name)
528 }
529
530 /// The `charset` parameter. Compare it case-insensitively.
531 ///
532 /// # Examples
533 ///
534 /// ```
535 /// let mt = parse_net::parse_media_type(b"text/html; charset=\"UTF-8\"")?;
536 /// assert!(mt.charset().is_some_and(|c| c.eq_ignore_ascii_case(b"utf-8")));
537 /// # Ok::<(), parse_net::ParseError>(())
538 /// ```
539 #[must_use]
540 pub fn charset(&self) -> Option<Word<'a>> {
541 self.params.get("charset")
542 }
543
544 /// The `boundary` parameter, validated by RFC 2046 section 5.1.1: 1 to 70
545 /// `bchars`, the last not a space. `Ok(None)` when absent.
546 ///
547 /// # Errors
548 ///
549 /// [`ErrorKind::InvalidBoundary`], with the offset inside the boundary
550 /// value.
551 ///
552 /// # Examples
553 ///
554 /// ```
555 /// use parse_net::parse_media_type;
556 ///
557 /// let mt = parse_media_type(b"multipart/form-data; boundary=\"a b\"")?;
558 /// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"a b"[..]));
559 /// assert_eq!(parse_media_type(b"text/plain")?.boundary()?, None);
560 /// assert!(parse_media_type(b"multipart/mixed; boundary=\"ab \"")?.boundary().is_err());
561 /// # Ok::<(), parse_net::ParseError>(())
562 /// ```
563 pub fn boundary(&self) -> Result<Option<Boundary<'a>>> {
564 let Some(word) = self.params.get("boundary") else {
565 return Ok(None);
566 };
567 let bytes = match word {
568 Word::Token(t) => t,
569 // `\` is not a bchar, so a quoted boundary cannot carry escapes.
570 Word::Quoted(q) if q.has_escapes() => {
571 let at = q.raw().iter().position(|&b| b == b'\\').unwrap_or(0);
572 return err(ErrorKind::InvalidBoundary, at);
573 }
574 Word::Quoted(q) => q.raw(),
575 };
576 if bytes.is_empty() {
577 return err(ErrorKind::InvalidBoundary, 0);
578 }
579 if let Some(at) = bytes
580 .iter()
581 .position(|&b| !(class::is_bchar_nospace(b) || b == b' '))
582 {
583 return err(ErrorKind::InvalidBoundary, at);
584 }
585 if bytes.len() > 70 {
586 return err(ErrorKind::InvalidBoundary, 70);
587 }
588 if bytes.last() == Some(&b' ') {
589 return err(ErrorKind::InvalidBoundary, bytes.len() - 1);
590 }
591 Ok(Some(Boundary(bytes)))
592 }
593
594 /// Whether this is `application/json` or has the `+json` suffix.
595 ///
596 /// # Examples
597 ///
598 /// ```
599 /// use parse_net::parse_media_type;
600 ///
601 /// assert!(parse_media_type(b"application/json")?.is_json());
602 /// assert!(parse_media_type(b"application/problem+json")?.is_json());
603 /// assert!(!parse_media_type(b"text/json")?.is_json());
604 /// # Ok::<(), parse_net::ParseError>(())
605 /// ```
606 #[must_use]
607 pub fn is_json(&self) -> bool {
608 self.is("application", "json")
609 || self
610 .suffix()
611 .is_some_and(|s| s.eq_ignore_ascii_case(b"json"))
612 }
613
614 /// Whether this is `application/x-www-form-urlencoded`.
615 ///
616 /// # Examples
617 ///
618 /// ```
619 /// let mt = parse_net::parse_media_type(b"application/x-www-form-urlencoded; charset=utf-8")?;
620 /// assert!(mt.is_form_urlencoded());
621 /// # Ok::<(), parse_net::ParseError>(())
622 /// ```
623 #[must_use]
624 pub fn is_form_urlencoded(&self) -> bool {
625 self.is("application", "x-www-form-urlencoded")
626 }
627
628 /// Whether this is `multipart/form-data`.
629 ///
630 /// # Examples
631 ///
632 /// ```
633 /// use parse_net::parse_media_type;
634 ///
635 /// assert!(parse_media_type(b"multipart/form-data; boundary=x")?.is_multipart_form_data());
636 /// assert!(!parse_media_type(b"multipart/mixed; boundary=x")?.is_multipart_form_data());
637 /// # Ok::<(), parse_net::ParseError>(())
638 /// ```
639 #[must_use]
640 pub fn is_multipart_form_data(&self) -> bool {
641 self.is("multipart", "form-data")
642 }
643
644 /// Writes the media type with the type, subtype, and parameter names in
645 /// lowercase and the values as sent, separated by `; `.
646 ///
647 /// # Errors
648 ///
649 /// [`WriteError::Full`]; nothing is written when the sink reports its
650 /// room.
651 ///
652 /// # Examples
653 ///
654 /// ```
655 /// use parse_net::{parse_media_type, SliceSink};
656 ///
657 /// let mt = parse_media_type(b"Text/HTML;Charset=\"UTF-8\"")?;
658 /// let mut buf = [0u8; 64];
659 /// let mut out = SliceSink::new(&mut buf);
660 /// mt.write_canonical(&mut out).expect("room");
661 /// assert_eq!(out.written(), b"text/html; charset=\"UTF-8\"");
662 /// # Ok::<(), parse_net::ParseError>(())
663 /// ```
664 pub fn write_canonical(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
665 let mut len = self.type_.len() + 1 + self.subtype.len();
666 for p in self.params.iter() {
667 len = len.saturating_add(3 + p.name.len() + raw_word_len(&p.value));
668 }
669 reserve(out, len)?;
670 put_lower(self.type_, out)?;
671 out.put(b"/")?;
672 put_lower(self.subtype, out)?;
673 for p in self.params.iter() {
674 out.put(b"; ")?;
675 put_lower(p.name, out)?;
676 out.put(b"=")?;
677 match p.value {
678 Word::Token(t) => out.put(t)?,
679 Word::Quoted(q) => {
680 out.put(b"\"")?;
681 out.put(q.raw())?;
682 out.put(b"\"")?;
683 }
684 }
685 }
686 Ok(())
687 }
688}
689
690/// The length of a word written as it was sent.
691fn raw_word_len(w: &Word<'_>) -> usize {
692 match w {
693 Word::Token(t) => t.len(),
694 Word::Quoted(q) => q.raw().len() + 2,
695 }
696}
697
698/// Writes `s` in ASCII lowercase, through a small stack buffer.
699fn put_lower(s: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
700 let mut buf = [0u8; 64];
701 for chunk in s.chunks(buf.len()) {
702 let dst = buf.get_mut(..chunk.len()).unwrap_or(&mut []);
703 for (d, &c) in dst.iter_mut().zip(chunk) {
704 *d = c.to_ascii_lowercase();
705 }
706 out.put(dst)?;
707 }
708 Ok(())
709}
710
711/// A validated multipart boundary, unquoted.
712///
713/// # Examples
714///
715/// ```
716/// let mt = parse_net::parse_media_type(b"multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")?;
717/// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"gc0p4Jq0M2Yt08j34c0p"[..]));
718/// # Ok::<(), parse_net::ParseError>(())
719/// ```
720#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
721pub struct Boundary<'a>(&'a [u8]);
722
723impl<'a> Boundary<'a> {
724 /// The boundary bytes.
725 ///
726 /// # Examples
727 ///
728 /// ```
729 /// let mt = parse_net::parse_media_type(b"multipart/mixed; boundary=\"simple boundary\"")?;
730 /// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"simple boundary"[..]));
731 /// # Ok::<(), parse_net::ParseError>(())
732 /// ```
733 #[must_use]
734 pub const fn as_bytes(&self) -> &'a [u8] {
735 self.0
736 }
737}
738
739#[cfg(test)]
740mod tests {
741 use std::{vec, vec::Vec};
742
743 use super::*;
744 use crate::write::SliceSink;
745
746 fn mt(s: &[u8]) -> Result<MediaType<'_>> {
747 parse_media_type(s)
748 }
749
750 #[test]
751 fn test_media_type_edge_cases() {
752 assert!(mt(b"text/html").unwrap().is("text", "html"));
753 assert!(mt(b"TEXT/HTML").unwrap().essence_is("text/html"));
754 assert!(mt(b"text/html;").unwrap().params().is_empty());
755 assert!(mt(b"text/html ;charset=utf-8").unwrap().charset().is_some());
756 assert!(mt(b" text/html \t").is_ok());
757 let e = mt(b"text/ html").unwrap_err();
758 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 5));
759 let e = mt(b"text /html").unwrap_err();
760 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 4));
761 assert_eq!(
762 mt(b"text/").unwrap_err().kind(),
763 ErrorKind::InvalidMediaType
764 );
765 assert_eq!(
766 mt(b"/html").unwrap_err().kind(),
767 ErrorKind::InvalidMediaType
768 );
769 assert_eq!(mt(b"").unwrap_err().kind(), ErrorKind::InvalidMediaType);
770 assert_eq!(mt(b"text").unwrap_err().kind(), ErrorKind::InvalidMediaType);
771 assert_eq!(mt(b"*/*").unwrap_err().kind(), ErrorKind::Wildcard);
772 let e = mt(b"text/*").unwrap_err();
773 assert_eq!((e.kind(), e.offset()), (ErrorKind::Wildcard, 5));
774 assert_eq!(
775 mt(b"text/html x").unwrap_err().kind(),
776 ErrorKind::InvalidMediaType
777 );
778 assert_eq!(mt(b"text/html\r").unwrap_err().kind(), ErrorKind::BareCr);
779 assert_eq!(
780 mt(b"text/html;a=b\nc").unwrap_err().kind(),
781 ErrorKind::LineFeed
782 );
783 let e = mt(b"text/html; charset=a; Charset=b").unwrap_err();
784 assert_eq!((e.kind(), e.offset()), (ErrorKind::DuplicateParameter, 22));
785 }
786
787 #[test]
788 fn test_media_type_suffix_and_predicates() {
789 let v = mt(b"application/vnd.api+json").unwrap();
790 assert_eq!(v.suffix(), Some(&b"json"[..]));
791 assert!(v.is_json());
792 assert!(mt(b"application/json").unwrap().is_json());
793 assert!(!mt(b"application/jsonx").unwrap().is_json());
794 assert_eq!(mt(b"application/a+").unwrap().suffix(), None);
795 assert!(
796 mt(b"application/x-www-form-urlencoded")
797 .unwrap()
798 .is_form_urlencoded()
799 );
800 assert!(!mt(b"text/html").unwrap().essence_is("texthtml"));
801 }
802
803 fn boundary(s: &[u8]) -> Result<Option<Vec<u8>>> {
804 Ok(mt(s)?.boundary()?.map(|b| b.as_bytes().to_vec()))
805 }
806
807 #[test]
808 fn test_boundary_rfc2046_rules() {
809 assert_eq!(boundary(b"multipart/form-data").unwrap(), None);
810 assert_eq!(
811 boundary(b"multipart/form-data; boundary=")
812 .unwrap_err()
813 .kind(),
814 ErrorKind::MissingValue
815 );
816 assert_eq!(
817 boundary(b"multipart/form-data; boundary=\"\"")
818 .unwrap_err()
819 .kind(),
820 ErrorKind::InvalidBoundary
821 );
822 let seventy = vec![b'a'; 70];
823 let mut v = b"multipart/mixed; boundary=".to_vec();
824 v.extend_from_slice(&seventy);
825 assert_eq!(boundary(&v).unwrap(), Some(seventy));
826 v.push(b'a');
827 assert_eq!(boundary(&v).unwrap_err().kind(), ErrorKind::InvalidBoundary);
828 assert_eq!(
829 boundary(b"multipart/mixed; boundary=\"ab \"")
830 .unwrap_err()
831 .kind(),
832 ErrorKind::InvalidBoundary
833 );
834 assert_eq!(
835 boundary(b"multipart/mixed; boundary=\"a b\"").unwrap(),
836 Some(b"a b".to_vec())
837 );
838 assert_eq!(
839 boundary(b"multipart/mixed; boundary=\"a\\\\b\"")
840 .unwrap_err()
841 .kind(),
842 ErrorKind::InvalidBoundary
843 );
844 assert_eq!(
845 boundary(b"multipart/mixed; boundary=a*b")
846 .unwrap_err()
847 .kind(),
848 ErrorKind::InvalidBoundary
849 );
850 assert_eq!(
851 mt(b"multipart/mixed; boundary=a; boundary=b")
852 .unwrap_err()
853 .kind(),
854 ErrorKind::DuplicateParameter
855 );
856 }
857
858 #[test]
859 fn test_media_write_refuses_bad_names_and_round_trips() {
860 let mut buf = [0u8; 128];
861 let mut out = SliceSink::new(&mut buf);
862 assert_eq!(write("text", "*", &[], &mut out), Err(WriteError::NotToken));
863 assert_eq!(
864 write("te xt", "a", &[], &mut out),
865 Err(WriteError::NotToken)
866 );
867 assert_eq!(
868 write("a", "b", &[("x y", "1")], &mut out),
869 Err(WriteError::NotToken)
870 );
871 assert_eq!(
872 write("a", "b", &[("x", "1\r\n")], &mut out),
873 Err(WriteError::Forbidden { at: 1 })
874 );
875 assert_eq!(
876 write("a", "b", &[("x", "1"), ("X", "2")], &mut out),
877 Err(WriteError::DuplicateName)
878 );
879 assert!(out.is_empty());
880 write("a", "b", &[("x", "1 2"), ("y", "")], &mut out).unwrap();
881 let back = mt(out.written()).unwrap();
882 assert!(back.param("x").unwrap().eq_bytes(b"1 2"));
883 assert!(back.param("y").unwrap().is_empty());
884 }
885
886 #[test]
887 fn test_write_canonical_lowercases_names_only() {
888 let v = mt(b"TEXT/Plain; FORMAT=Flowed;x=\"A\\\"B\"").unwrap();
889 let mut buf = [0u8; 64];
890 let mut out = SliceSink::new(&mut buf);
891 v.write_canonical(&mut out).unwrap();
892 assert_eq!(out.written(), b"text/plain; format=Flowed; x=\"A\\\"B\"");
893 let mut small = [0u8; 4];
894 let mut out = SliceSink::new(&mut small);
895 assert_eq!(v.write_canonical(&mut out), Err(WriteError::Full));
896 assert!(out.is_empty());
897 }
898}