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)
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 let type_ok = self.type_ == b"*" || self.type_.eq_ignore_ascii_case(offer.type_());
267 let sub_ok = self.subtype == b"*" || self.subtype.eq_ignore_ascii_case(offer.subtype());
268 type_ok
269 && sub_ok
270 && self.params.iter().all(|p| {
271 let charset = p.name.eq_ignore_ascii_case(b"charset");
272 offer.params().iter().any(|o| {
273 o.name.eq_ignore_ascii_case(p.name) && p.value.same_value(&o.value, charset)
274 })
275 })
276 }
277
278 /// How specific the range is: 4 for a type with parameters, 3 for an
279 /// exact type, 2 for `type/*`, 1 for `*/*`.
280 ///
281 /// # Examples
282 ///
283 /// ```
284 /// use parse_net::{media, Limits};
285 ///
286 /// let s = |v: &[u8]| media::parse_range(v, &Limits::DEFAULT).map(|r| r.specificity());
287 /// assert_eq!((s(b"*/*")?, s(b"image/*")?, s(b"image/png")?, s(b"image/png;x=1")?), (1, 2, 3, 4));
288 /// # Ok::<(), parse_net::ParseError>(())
289 /// ```
290 #[must_use]
291 pub const fn specificity(&self) -> u8 {
292 if matches!(self.type_, [b'*']) {
293 1
294 } else if matches!(self.subtype, [b'*']) {
295 2
296 } else if self.params.is_empty() {
297 3
298 } else {
299 4
300 }
301 }
302
303 /// The weight (`q`), 1 when absent.
304 ///
305 /// # Examples
306 ///
307 /// ```
308 /// use parse_net::{media, Limits, QValue};
309 ///
310 /// assert_eq!(media::parse_range(b"text/html;q=0.25", &Limits::DEFAULT)?.weight().thousandths(), 250);
311 /// assert_eq!(media::parse_range(b"text/html", &Limits::DEFAULT)?.weight(), QValue::ONE);
312 /// # Ok::<(), parse_net::ParseError>(())
313 /// ```
314 #[must_use]
315 pub const fn weight(&self) -> QValue {
316 self.weight
317 }
318
319 /// The media-type parameters: those before `q`.
320 ///
321 /// # Examples
322 ///
323 /// ```
324 /// use parse_net::{media, Limits};
325 ///
326 /// let r = media::parse_range(b"text/html;level=1;q=0.5", &Limits::DEFAULT)?;
327 /// assert_eq!(r.params().get("level").and_then(|w| w.as_str()), Some("1"));
328 /// assert!(r.params().get("q").is_none());
329 /// # Ok::<(), parse_net::ParseError>(())
330 /// ```
331 #[must_use]
332 pub const fn params(&self) -> Params<'a> {
333 self.params
334 }
335}
336
337/// Writes `type/subtype` and then `; name=value` for each pair, values as a
338/// token when they are one and as a quoted string otherwise.
339///
340/// # Errors
341///
342/// [`WriteError::NotToken`] for a type, subtype, or name that is not a token
343/// (or is `*`); [`WriteError::DuplicateName`] for two parameters with the
344/// same name (ASCII case-insensitive), which parsers refuse;
345/// [`WriteError::Forbidden`] for a control byte in a value;
346/// [`WriteError::Full`]. Nothing is written on error when the sink reports
347/// its room.
348///
349/// # Examples
350///
351/// ```
352/// use parse_net::{media, SliceSink};
353///
354/// let mut buf = [0u8; 64];
355/// let mut out = SliceSink::new(&mut buf);
356/// media::write("multipart", "form-data", &[("boundary", "a b")], &mut out)?;
357/// assert_eq!(out.written(), b"multipart/form-data; boundary=\"a b\"");
358/// # Ok::<(), parse_net::WriteError>(())
359/// ```
360pub fn write(
361 type_: &str,
362 subtype: &str,
363 params: &[(&str, &str)],
364 out: &mut impl Sink,
365) -> core::result::Result<(), WriteError> {
366 let (t, s) = (type_.as_bytes(), subtype.as_bytes());
367 if !is_token(t) || !is_token(s) || t == b"*" || s == b"*" {
368 return Err(WriteError::NotToken);
369 }
370 unique_names(params)?;
371 let mut len = t.len().saturating_add(1).saturating_add(s.len());
372 for (name, value) in params {
373 if !is_token(name.as_bytes()) {
374 return Err(WriteError::NotToken);
375 }
376 let v = word_len(value.as_bytes())?;
377 len = len
378 .saturating_add(3)
379 .saturating_add(name.len())
380 .saturating_add(v);
381 }
382 reserve(out, len)?;
383 out.put(t)?;
384 out.put(b"/")?;
385 out.put(s)?;
386 for (name, value) in params {
387 out.put(b"; ")?;
388 out.put(name.as_bytes())?;
389 out.put(b"=")?;
390 write_word(value.as_bytes(), out)?;
391 }
392 Ok(())
393}
394
395/// A validated media type.
396///
397/// # Examples
398///
399/// ```
400/// let ld = parse_net::parse_media_type(b"application/ld+json")?;
401/// assert_eq!(ld.suffix(), Some(&b"json"[..]));
402/// assert!(ld.is_json());
403/// # Ok::<(), parse_net::ParseError>(())
404/// ```
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
406pub struct MediaType<'a> {
407 pub(crate) type_: &'a [u8],
408 pub(crate) subtype: &'a [u8],
409 pub(crate) params: Params<'a>,
410}
411
412impl<'a> MediaType<'a> {
413 /// The type, as sent.
414 ///
415 /// # Examples
416 ///
417 /// ```
418 /// let mt = parse_net::parse_media_type(b"Text/HTML")?;
419 /// assert_eq!(mt.type_(), b"Text");
420 /// # Ok::<(), parse_net::ParseError>(())
421 /// ```
422 #[must_use]
423 pub const fn type_(&self) -> &'a [u8] {
424 self.type_
425 }
426
427 /// The subtype, as sent.
428 ///
429 /// # Examples
430 ///
431 /// ```
432 /// let mt = parse_net::parse_media_type(b"Text/HTML; charset=utf-8")?;
433 /// assert_eq!(mt.subtype(), b"HTML");
434 /// # Ok::<(), parse_net::ParseError>(())
435 /// ```
436 #[must_use]
437 pub const fn subtype(&self) -> &'a [u8] {
438 self.subtype
439 }
440
441 /// The structured syntax suffix: the part of the subtype after its last
442 /// `+` (`json` of `ld+json`), when there is one.
443 ///
444 /// # Examples
445 ///
446 /// ```
447 /// use parse_net::parse_media_type;
448 ///
449 /// assert_eq!(parse_media_type(b"image/svg+xml")?.suffix(), Some(&b"xml"[..]));
450 /// assert_eq!(parse_media_type(b"text/plain")?.suffix(), None);
451 /// # Ok::<(), parse_net::ParseError>(())
452 /// ```
453 #[must_use]
454 pub fn suffix(&self) -> Option<&'a [u8]> {
455 let plus = self.subtype.iter().rposition(|&b| b == b'+')?;
456 self.subtype.get(plus + 1..).filter(|s| !s.is_empty())
457 }
458
459 /// Whether `type/subtype` equals `essence`, ignoring ASCII case.
460 ///
461 /// # Examples
462 ///
463 /// ```
464 /// let mt = parse_net::parse_media_type(b"Application/JSON; charset=utf-8")?;
465 /// assert!(mt.essence_is("application/json"));
466 /// assert!(!mt.essence_is("application"));
467 /// # Ok::<(), parse_net::ParseError>(())
468 /// ```
469 #[must_use]
470 pub fn essence_is(&self, essence: &str) -> bool {
471 match essence.split_once('/') {
472 Some((t, s)) => self.is(t, s),
473 None => false,
474 }
475 }
476
477 /// Whether the type and subtype equal `type_` and `subtype`, ignoring
478 /// ASCII case.
479 ///
480 /// # Examples
481 ///
482 /// ```
483 /// let mt = parse_net::parse_media_type(b"TEXT/html")?;
484 /// assert!(mt.is("text", "HTML") && !mt.is("text", "plain"));
485 /// # Ok::<(), parse_net::ParseError>(())
486 /// ```
487 #[must_use]
488 pub fn is(&self, type_: &str, subtype: &str) -> bool {
489 self.type_.eq_ignore_ascii_case(type_.as_bytes())
490 && self.subtype.eq_ignore_ascii_case(subtype.as_bytes())
491 }
492
493 /// The parameters.
494 ///
495 /// # Examples
496 ///
497 /// ```
498 /// let mt = parse_net::parse_media_type(b"text/plain; charset=utf-8; format=flowed")?;
499 /// assert_eq!(mt.params().len(), 2);
500 /// # Ok::<(), parse_net::ParseError>(())
501 /// ```
502 #[must_use]
503 pub const fn params(&self) -> Params<'a> {
504 self.params
505 }
506
507 /// The value of the parameter `name` (ASCII case-insensitive).
508 ///
509 /// # Examples
510 ///
511 /// ```
512 /// let mt = parse_net::parse_media_type(b"text/plain; format=flowed")?;
513 /// assert_eq!(mt.param("Format").and_then(|w| w.as_str()), Some("flowed"));
514 /// assert!(mt.param("delsp").is_none());
515 /// # Ok::<(), parse_net::ParseError>(())
516 /// ```
517 #[must_use]
518 pub fn param(&self, name: &str) -> Option<Word<'a>> {
519 self.params.get(name)
520 }
521
522 /// The `charset` parameter. Compare it case-insensitively.
523 ///
524 /// # Examples
525 ///
526 /// ```
527 /// let mt = parse_net::parse_media_type(b"text/html; charset=\"UTF-8\"")?;
528 /// assert!(mt.charset().is_some_and(|c| c.eq_ignore_ascii_case(b"utf-8")));
529 /// # Ok::<(), parse_net::ParseError>(())
530 /// ```
531 #[must_use]
532 pub fn charset(&self) -> Option<Word<'a>> {
533 self.params.get("charset")
534 }
535
536 /// The `boundary` parameter, validated by RFC 2046 section 5.1.1: 1 to 70
537 /// `bchars`, the last not a space. `Ok(None)` when absent.
538 ///
539 /// # Errors
540 ///
541 /// [`ErrorKind::InvalidBoundary`], with the offset inside the boundary
542 /// value.
543 ///
544 /// # Examples
545 ///
546 /// ```
547 /// use parse_net::parse_media_type;
548 ///
549 /// let mt = parse_media_type(b"multipart/form-data; boundary=\"a b\"")?;
550 /// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"a b"[..]));
551 /// assert_eq!(parse_media_type(b"text/plain")?.boundary()?, None);
552 /// assert!(parse_media_type(b"multipart/mixed; boundary=\"ab \"")?.boundary().is_err());
553 /// # Ok::<(), parse_net::ParseError>(())
554 /// ```
555 pub fn boundary(&self) -> Result<Option<Boundary<'a>>> {
556 let Some(word) = self.params.get("boundary") else {
557 return Ok(None);
558 };
559 let bytes = match word {
560 Word::Token(t) => t,
561 // `\` is not a bchar, so a quoted boundary cannot carry escapes.
562 Word::Quoted(q) if q.has_escapes() => {
563 let at = q.raw().iter().position(|&b| b == b'\\').unwrap_or(0);
564 return err(ErrorKind::InvalidBoundary, at);
565 }
566 Word::Quoted(q) => q.raw(),
567 };
568 if bytes.is_empty() {
569 return err(ErrorKind::InvalidBoundary, 0);
570 }
571 if let Some(at) = bytes
572 .iter()
573 .position(|&b| !(class::is_bchar_nospace(b) || b == b' '))
574 {
575 return err(ErrorKind::InvalidBoundary, at);
576 }
577 if bytes.len() > 70 {
578 return err(ErrorKind::InvalidBoundary, 70);
579 }
580 if bytes.last() == Some(&b' ') {
581 return err(ErrorKind::InvalidBoundary, bytes.len() - 1);
582 }
583 Ok(Some(Boundary(bytes)))
584 }
585
586 /// Whether this is `application/json` or has the `+json` suffix.
587 ///
588 /// # Examples
589 ///
590 /// ```
591 /// use parse_net::parse_media_type;
592 ///
593 /// assert!(parse_media_type(b"application/json")?.is_json());
594 /// assert!(parse_media_type(b"application/problem+json")?.is_json());
595 /// assert!(!parse_media_type(b"text/json")?.is_json());
596 /// # Ok::<(), parse_net::ParseError>(())
597 /// ```
598 #[must_use]
599 pub fn is_json(&self) -> bool {
600 self.is("application", "json")
601 || self
602 .suffix()
603 .is_some_and(|s| s.eq_ignore_ascii_case(b"json"))
604 }
605
606 /// Whether this is `application/x-www-form-urlencoded`.
607 ///
608 /// # Examples
609 ///
610 /// ```
611 /// let mt = parse_net::parse_media_type(b"application/x-www-form-urlencoded; charset=utf-8")?;
612 /// assert!(mt.is_form_urlencoded());
613 /// # Ok::<(), parse_net::ParseError>(())
614 /// ```
615 #[must_use]
616 pub fn is_form_urlencoded(&self) -> bool {
617 self.is("application", "x-www-form-urlencoded")
618 }
619
620 /// Whether this is `multipart/form-data`.
621 ///
622 /// # Examples
623 ///
624 /// ```
625 /// use parse_net::parse_media_type;
626 ///
627 /// assert!(parse_media_type(b"multipart/form-data; boundary=x")?.is_multipart_form_data());
628 /// assert!(!parse_media_type(b"multipart/mixed; boundary=x")?.is_multipart_form_data());
629 /// # Ok::<(), parse_net::ParseError>(())
630 /// ```
631 #[must_use]
632 pub fn is_multipart_form_data(&self) -> bool {
633 self.is("multipart", "form-data")
634 }
635
636 /// Writes the media type with the type, subtype, and parameter names in
637 /// lowercase and the values as sent, separated by `; `.
638 ///
639 /// # Errors
640 ///
641 /// [`WriteError::Full`]; nothing is written when the sink reports its
642 /// room.
643 ///
644 /// # Examples
645 ///
646 /// ```
647 /// use parse_net::{parse_media_type, SliceSink};
648 ///
649 /// let mt = parse_media_type(b"Text/HTML;Charset=\"UTF-8\"")?;
650 /// let mut buf = [0u8; 64];
651 /// let mut out = SliceSink::new(&mut buf);
652 /// mt.write_canonical(&mut out).expect("room");
653 /// assert_eq!(out.written(), b"text/html; charset=\"UTF-8\"");
654 /// # Ok::<(), parse_net::ParseError>(())
655 /// ```
656 pub fn write_canonical(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
657 let mut len = self.type_.len() + 1 + self.subtype.len();
658 for p in self.params.iter() {
659 len = len.saturating_add(3 + p.name.len() + raw_word_len(&p.value));
660 }
661 reserve(out, len)?;
662 put_lower(self.type_, out)?;
663 out.put(b"/")?;
664 put_lower(self.subtype, out)?;
665 for p in self.params.iter() {
666 out.put(b"; ")?;
667 put_lower(p.name, out)?;
668 out.put(b"=")?;
669 match p.value {
670 Word::Token(t) => out.put(t)?,
671 Word::Quoted(q) => {
672 out.put(b"\"")?;
673 out.put(q.raw())?;
674 out.put(b"\"")?;
675 }
676 }
677 }
678 Ok(())
679 }
680}
681
682/// The length of a word written as it was sent.
683fn raw_word_len(w: &Word<'_>) -> usize {
684 match w {
685 Word::Token(t) => t.len(),
686 Word::Quoted(q) => q.raw().len() + 2,
687 }
688}
689
690/// Writes `s` in ASCII lowercase, through a small stack buffer.
691fn put_lower(s: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
692 let mut buf = [0u8; 64];
693 for chunk in s.chunks(buf.len()) {
694 let dst = buf.get_mut(..chunk.len()).unwrap_or(&mut []);
695 for (d, &c) in dst.iter_mut().zip(chunk) {
696 *d = c.to_ascii_lowercase();
697 }
698 out.put(dst)?;
699 }
700 Ok(())
701}
702
703/// A validated multipart boundary, unquoted.
704///
705/// # Examples
706///
707/// ```
708/// let mt = parse_net::parse_media_type(b"multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")?;
709/// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"gc0p4Jq0M2Yt08j34c0p"[..]));
710/// # Ok::<(), parse_net::ParseError>(())
711/// ```
712#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
713pub struct Boundary<'a>(&'a [u8]);
714
715impl<'a> Boundary<'a> {
716 /// The boundary bytes.
717 ///
718 /// # Examples
719 ///
720 /// ```
721 /// let mt = parse_net::parse_media_type(b"multipart/mixed; boundary=\"simple boundary\"")?;
722 /// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"simple boundary"[..]));
723 /// # Ok::<(), parse_net::ParseError>(())
724 /// ```
725 #[must_use]
726 pub const fn as_bytes(&self) -> &'a [u8] {
727 self.0
728 }
729}
730
731#[cfg(test)]
732mod tests {
733 use std::{vec, vec::Vec};
734
735 use super::*;
736 use crate::write::SliceSink;
737
738 fn mt(s: &[u8]) -> Result<MediaType<'_>> {
739 parse_media_type(s)
740 }
741
742 #[test]
743 fn test_media_type_edge_cases() {
744 assert!(mt(b"text/html").unwrap().is("text", "html"));
745 assert!(mt(b"TEXT/HTML").unwrap().essence_is("text/html"));
746 assert!(mt(b"text/html;").unwrap().params().is_empty());
747 assert!(mt(b"text/html ;charset=utf-8").unwrap().charset().is_some());
748 assert!(mt(b" text/html \t").is_ok());
749 let e = mt(b"text/ html").unwrap_err();
750 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 5));
751 let e = mt(b"text /html").unwrap_err();
752 assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 4));
753 assert_eq!(
754 mt(b"text/").unwrap_err().kind(),
755 ErrorKind::InvalidMediaType
756 );
757 assert_eq!(
758 mt(b"/html").unwrap_err().kind(),
759 ErrorKind::InvalidMediaType
760 );
761 assert_eq!(mt(b"").unwrap_err().kind(), ErrorKind::InvalidMediaType);
762 assert_eq!(mt(b"text").unwrap_err().kind(), ErrorKind::InvalidMediaType);
763 assert_eq!(mt(b"*/*").unwrap_err().kind(), ErrorKind::Wildcard);
764 let e = mt(b"text/*").unwrap_err();
765 assert_eq!((e.kind(), e.offset()), (ErrorKind::Wildcard, 5));
766 assert_eq!(
767 mt(b"text/html x").unwrap_err().kind(),
768 ErrorKind::InvalidMediaType
769 );
770 assert_eq!(mt(b"text/html\r").unwrap_err().kind(), ErrorKind::BareCr);
771 assert_eq!(
772 mt(b"text/html;a=b\nc").unwrap_err().kind(),
773 ErrorKind::LineFeed
774 );
775 let e = mt(b"text/html; charset=a; Charset=b").unwrap_err();
776 assert_eq!((e.kind(), e.offset()), (ErrorKind::DuplicateParameter, 22));
777 }
778
779 #[test]
780 fn test_media_type_suffix_and_predicates() {
781 let v = mt(b"application/vnd.api+json").unwrap();
782 assert_eq!(v.suffix(), Some(&b"json"[..]));
783 assert!(v.is_json());
784 assert!(mt(b"application/json").unwrap().is_json());
785 assert!(!mt(b"application/jsonx").unwrap().is_json());
786 assert_eq!(mt(b"application/a+").unwrap().suffix(), None);
787 assert!(
788 mt(b"application/x-www-form-urlencoded")
789 .unwrap()
790 .is_form_urlencoded()
791 );
792 assert!(!mt(b"text/html").unwrap().essence_is("texthtml"));
793 }
794
795 fn boundary(s: &[u8]) -> Result<Option<Vec<u8>>> {
796 Ok(mt(s)?.boundary()?.map(|b| b.as_bytes().to_vec()))
797 }
798
799 #[test]
800 fn test_boundary_rfc2046_rules() {
801 assert_eq!(boundary(b"multipart/form-data").unwrap(), None);
802 assert_eq!(
803 boundary(b"multipart/form-data; boundary=")
804 .unwrap_err()
805 .kind(),
806 ErrorKind::MissingValue
807 );
808 assert_eq!(
809 boundary(b"multipart/form-data; boundary=\"\"")
810 .unwrap_err()
811 .kind(),
812 ErrorKind::InvalidBoundary
813 );
814 let seventy = vec![b'a'; 70];
815 let mut v = b"multipart/mixed; boundary=".to_vec();
816 v.extend_from_slice(&seventy);
817 assert_eq!(boundary(&v).unwrap(), Some(seventy));
818 v.push(b'a');
819 assert_eq!(boundary(&v).unwrap_err().kind(), ErrorKind::InvalidBoundary);
820 assert_eq!(
821 boundary(b"multipart/mixed; boundary=\"ab \"")
822 .unwrap_err()
823 .kind(),
824 ErrorKind::InvalidBoundary
825 );
826 assert_eq!(
827 boundary(b"multipart/mixed; boundary=\"a b\"").unwrap(),
828 Some(b"a b".to_vec())
829 );
830 assert_eq!(
831 boundary(b"multipart/mixed; boundary=\"a\\\\b\"")
832 .unwrap_err()
833 .kind(),
834 ErrorKind::InvalidBoundary
835 );
836 assert_eq!(
837 boundary(b"multipart/mixed; boundary=a*b")
838 .unwrap_err()
839 .kind(),
840 ErrorKind::InvalidBoundary
841 );
842 assert_eq!(
843 mt(b"multipart/mixed; boundary=a; boundary=b")
844 .unwrap_err()
845 .kind(),
846 ErrorKind::DuplicateParameter
847 );
848 }
849
850 #[test]
851 fn test_media_write_refuses_bad_names_and_round_trips() {
852 let mut buf = [0u8; 128];
853 let mut out = SliceSink::new(&mut buf);
854 assert_eq!(write("text", "*", &[], &mut out), Err(WriteError::NotToken));
855 assert_eq!(
856 write("te xt", "a", &[], &mut out),
857 Err(WriteError::NotToken)
858 );
859 assert_eq!(
860 write("a", "b", &[("x y", "1")], &mut out),
861 Err(WriteError::NotToken)
862 );
863 assert_eq!(
864 write("a", "b", &[("x", "1\r\n")], &mut out),
865 Err(WriteError::Forbidden { at: 1 })
866 );
867 assert_eq!(
868 write("a", "b", &[("x", "1"), ("X", "2")], &mut out),
869 Err(WriteError::DuplicateName)
870 );
871 assert!(out.is_empty());
872 write("a", "b", &[("x", "1 2"), ("y", "")], &mut out).unwrap();
873 let back = mt(out.written()).unwrap();
874 assert!(back.param("x").unwrap().eq_bytes(b"1 2"));
875 assert!(back.param("y").unwrap().is_empty());
876 }
877
878 #[test]
879 fn test_write_canonical_lowercases_names_only() {
880 let v = mt(b"TEXT/Plain; FORMAT=Flowed;x=\"A\\\"B\"").unwrap();
881 let mut buf = [0u8; 64];
882 let mut out = SliceSink::new(&mut buf);
883 v.write_canonical(&mut out).unwrap();
884 assert_eq!(out.written(), b"text/plain; format=Flowed; x=\"A\\\"B\"");
885 let mut small = [0u8; 4];
886 let mut out = SliceSink::new(&mut small);
887 assert_eq!(v.write_canonical(&mut out), Err(WriteError::Full));
888 assert!(out.is_empty());
889 }
890}