Skip to main content

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}