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)
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}