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    param::{self, Params},
31    scan::control_kind,
32    text::{Word, is_token, skip_ows, trim_ows},
33    write::{Sink, reserve, word_len, write_word},
34};
35
36/// Parses a `Content-Type` style media type with the default [`Limits`].
37///
38/// # Errors
39///
40/// As [`parse`].
41///
42/// # Examples
43///
44/// ```
45/// let mt = parse_net::parse_media_type(b"text/html; charset=UTF-8")?;
46/// assert!(mt.essence_is("text/html"));
47/// assert!(mt.charset().is_some_and(|c| c.eq_ignore_ascii_case(b"utf-8")));
48/// # Ok::<(), parse_net::ParseError>(())
49/// ```
50pub fn parse_media_type(value: &[u8]) -> Result<MediaType<'_>> {
51    parse(value, &Limits::DEFAULT)
52}
53
54/// The error for a byte that ends a media type where it may not.
55fn bad_byte(value: &[u8], at: usize) -> ParseError {
56    match value.get(at) {
57        Some(&b) if b < 0x20 || b == 0x7f => ParseError::new(control_kind(b), at),
58        _ => ParseError::new(ErrorKind::InvalidMediaType, at),
59    }
60}
61
62/// Parses `type "/" subtype` at the start of `value` (OWS already skipped);
63/// returns both and the bytes used.
64fn essence(value: &[u8], base: usize) -> Result<(&[u8], &[u8], usize)> {
65    let type_len = value.iter().take_while(|&&b| class::is_tchar(b)).count();
66    if type_len == 0 {
67        return Err(bad_byte(value, 0).shifted(base));
68    }
69    if value.get(type_len) != Some(&b'/') {
70        return Err(bad_byte(value, type_len).shifted(base));
71    }
72    let sub_start = type_len + 1;
73    let sub_len = value
74        .get(sub_start..)
75        .unwrap_or(&[])
76        .iter()
77        .take_while(|&&b| class::is_tchar(b))
78        .count();
79    if sub_len == 0 {
80        return Err(bad_byte(value, sub_start).shifted(base));
81    }
82    let type_ = value.get(..type_len).unwrap_or(&[]);
83    let subtype = value.get(sub_start..sub_start + sub_len).unwrap_or(&[]);
84    Ok((type_, subtype, sub_start + sub_len))
85}
86
87/// Parses a media type, as in `Content-Type`.
88///
89/// Type and subtype are tokens around `/`; OWS is allowed only before `;` and
90/// around the whole value; parameters follow the RFC 9110 grammar with the
91/// duplicate policy of `limits` (refusal by default, so `charset` or
92/// `boundary` given twice is refused).
93///
94/// # Errors
95///
96/// [`ErrorKind::InvalidMediaType`] for a missing `/`, an empty or non-token
97/// type or subtype, whitespace inside the essence, or bytes after the
98/// parameters; [`ErrorKind::Wildcard`] for `*` as type or subtype; a control
99/// kind for a control byte; the parameter errors of
100/// [`param::parse`].
101///
102/// # Examples
103///
104/// ```
105/// use parse_net::{media, ErrorKind, Limits};
106///
107/// let err = media::parse(b"text/ html", &Limits::DEFAULT).unwrap_err();
108/// assert_eq!((err.kind(), err.offset()), (ErrorKind::InvalidMediaType, 5));
109/// assert_eq!(err.to_string(), "invalid media type at offset 5");
110/// ```
111pub fn parse<'a>(value: &'a [u8], limits: &Limits) -> Result<MediaType<'a>> {
112    let lead = skip_ows(value);
113    let trimmed = trim_ows(value);
114    let (type_, subtype, used) = essence(trimmed, lead)?;
115    if type_ == b"*" {
116        return err(ErrorKind::Wildcard, lead);
117    }
118    if subtype == b"*" {
119        return err(ErrorKind::Wildcard, lead + type_.len() + 1);
120    }
121    let rest = trimmed.get(used..).unwrap_or(&[]);
122    let (params, n) = param::parse(rest, limits).map_err(|e| e.shifted(lead + used))?;
123    let end = used + n;
124    if end < trimmed.len() {
125        let at = end + skip_ows(trimmed.get(end..).unwrap_or(&[]));
126        return Err(bad_byte(trimmed, at).shifted(lead));
127    }
128    Ok(MediaType {
129        type_,
130        subtype,
131        params,
132    })
133}
134
135/// Writes `type/subtype` and then `; name=value` for each pair, values as a
136/// token when they are one and as a quoted string otherwise.
137///
138/// # Errors
139///
140/// [`WriteError::NotToken`] for a type, subtype, or name that is not a token
141/// (or is `*`); [`WriteError::Forbidden`] for a control byte in a value;
142/// [`WriteError::Full`]. Nothing is written on error when the sink reports
143/// its room.
144///
145/// # Examples
146///
147/// ```
148/// use parse_net::{media, SliceSink};
149///
150/// let mut buf = [0u8; 64];
151/// let mut out = SliceSink::new(&mut buf);
152/// media::write("multipart", "form-data", &[("boundary", "a b")], &mut out)?;
153/// assert_eq!(out.written(), b"multipart/form-data; boundary=\"a b\"");
154/// # Ok::<(), parse_net::WriteError>(())
155/// ```
156pub fn write(
157    type_: &str,
158    subtype: &str,
159    params: &[(&str, &str)],
160    out: &mut impl Sink,
161) -> core::result::Result<(), WriteError> {
162    let (t, s) = (type_.as_bytes(), subtype.as_bytes());
163    if !is_token(t) || !is_token(s) || t == b"*" || s == b"*" {
164        return Err(WriteError::NotToken);
165    }
166    let mut len = t.len().saturating_add(1).saturating_add(s.len());
167    for (name, value) in params {
168        if !is_token(name.as_bytes()) {
169            return Err(WriteError::NotToken);
170        }
171        let v = word_len(value.as_bytes())?;
172        len = len
173            .saturating_add(3)
174            .saturating_add(name.len())
175            .saturating_add(v);
176    }
177    reserve(out, len)?;
178    out.put(t)?;
179    out.put(b"/")?;
180    out.put(s)?;
181    for (name, value) in params {
182        out.put(b"; ")?;
183        out.put(name.as_bytes())?;
184        out.put(b"=")?;
185        write_word(value.as_bytes(), out)?;
186    }
187    Ok(())
188}
189
190/// A validated media type.
191///
192/// # Examples
193///
194/// ```
195/// let ld = parse_net::parse_media_type(b"application/ld+json")?;
196/// assert_eq!(ld.suffix(), Some(&b"json"[..]));
197/// assert!(ld.is_json());
198/// # Ok::<(), parse_net::ParseError>(())
199/// ```
200#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
201pub struct MediaType<'a> {
202    type_: &'a [u8],
203    subtype: &'a [u8],
204    params: Params<'a>,
205}
206
207impl<'a> MediaType<'a> {
208    /// The type, as sent.
209    #[must_use]
210    pub const fn type_(&self) -> &'a [u8] {
211        self.type_
212    }
213
214    /// The subtype, as sent.
215    #[must_use]
216    pub const fn subtype(&self) -> &'a [u8] {
217        self.subtype
218    }
219
220    /// The structured syntax suffix: the part of the subtype after its last
221    /// `+` (`json` of `ld+json`), when there is one.
222    #[must_use]
223    pub fn suffix(&self) -> Option<&'a [u8]> {
224        let plus = self.subtype.iter().rposition(|&b| b == b'+')?;
225        self.subtype.get(plus + 1..).filter(|s| !s.is_empty())
226    }
227
228    /// Whether `type/subtype` equals `essence`, ignoring ASCII case.
229    #[must_use]
230    pub fn essence_is(&self, essence: &str) -> bool {
231        match essence.split_once('/') {
232            Some((t, s)) => self.is(t, s),
233            None => false,
234        }
235    }
236
237    /// Whether the type and subtype equal `type_` and `subtype`, ignoring
238    /// ASCII case.
239    #[must_use]
240    pub fn is(&self, type_: &str, subtype: &str) -> bool {
241        self.type_.eq_ignore_ascii_case(type_.as_bytes())
242            && self.subtype.eq_ignore_ascii_case(subtype.as_bytes())
243    }
244
245    /// The parameters.
246    #[must_use]
247    pub const fn params(&self) -> Params<'a> {
248        self.params
249    }
250
251    /// The value of the parameter `name` (ASCII case-insensitive).
252    #[must_use]
253    pub fn param(&self, name: &str) -> Option<Word<'a>> {
254        self.params.get(name)
255    }
256
257    /// The `charset` parameter. Compare it case-insensitively.
258    #[must_use]
259    pub fn charset(&self) -> Option<Word<'a>> {
260        self.params.get("charset")
261    }
262
263    /// The `boundary` parameter, validated by RFC 2046 section 5.1.1: 1 to 70
264    /// `bchars`, the last not a space. `Ok(None)` when absent.
265    ///
266    /// # Errors
267    ///
268    /// [`ErrorKind::InvalidBoundary`], with the offset inside the boundary
269    /// value.
270    pub fn boundary(&self) -> Result<Option<Boundary<'a>>> {
271        let Some(word) = self.params.get("boundary") else {
272            return Ok(None);
273        };
274        let bytes = match word {
275            Word::Token(t) => t,
276            // `\` is not a bchar, so a quoted boundary cannot carry escapes.
277            Word::Quoted(q) if q.has_escapes() => {
278                let at = q.raw().iter().position(|&b| b == b'\\').unwrap_or(0);
279                return err(ErrorKind::InvalidBoundary, at);
280            }
281            Word::Quoted(q) => q.raw(),
282        };
283        if bytes.is_empty() {
284            return err(ErrorKind::InvalidBoundary, 0);
285        }
286        if let Some(at) = bytes
287            .iter()
288            .position(|&b| !(class::is_bchar_nospace(b) || b == b' '))
289        {
290            return err(ErrorKind::InvalidBoundary, at);
291        }
292        if bytes.len() > 70 {
293            return err(ErrorKind::InvalidBoundary, 70);
294        }
295        if bytes.last() == Some(&b' ') {
296            return err(ErrorKind::InvalidBoundary, bytes.len() - 1);
297        }
298        Ok(Some(Boundary(bytes)))
299    }
300
301    /// Whether this is `application/json` or has the `+json` suffix.
302    #[must_use]
303    pub fn is_json(&self) -> bool {
304        self.is("application", "json")
305            || self
306                .suffix()
307                .is_some_and(|s| s.eq_ignore_ascii_case(b"json"))
308    }
309
310    /// Whether this is `application/x-www-form-urlencoded`.
311    #[must_use]
312    pub fn is_form_urlencoded(&self) -> bool {
313        self.is("application", "x-www-form-urlencoded")
314    }
315
316    /// Whether this is `multipart/form-data`.
317    #[must_use]
318    pub fn is_multipart_form_data(&self) -> bool {
319        self.is("multipart", "form-data")
320    }
321
322    /// Writes the media type with the type, subtype, and parameter names in
323    /// lowercase and the values as sent, separated by `; `.
324    ///
325    /// # Errors
326    ///
327    /// [`WriteError::Full`]; nothing is written when the sink reports its
328    /// room.
329    ///
330    /// # Examples
331    ///
332    /// ```
333    /// use parse_net::{parse_media_type, SliceSink};
334    ///
335    /// let mt = parse_media_type(b"Text/HTML;Charset=\"UTF-8\"")?;
336    /// let mut buf = [0u8; 64];
337    /// let mut out = SliceSink::new(&mut buf);
338    /// mt.write_canonical(&mut out).expect("room");
339    /// assert_eq!(out.written(), b"text/html; charset=\"UTF-8\"");
340    /// # Ok::<(), parse_net::ParseError>(())
341    /// ```
342    pub fn write_canonical(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
343        let mut len = self.type_.len() + 1 + self.subtype.len();
344        for p in self.params.iter() {
345            len = len.saturating_add(3 + p.name.len() + raw_word_len(&p.value));
346        }
347        reserve(out, len)?;
348        put_lower(self.type_, out)?;
349        out.put(b"/")?;
350        put_lower(self.subtype, out)?;
351        for p in self.params.iter() {
352            out.put(b"; ")?;
353            put_lower(p.name, out)?;
354            out.put(b"=")?;
355            match p.value {
356                Word::Token(t) => out.put(t)?,
357                Word::Quoted(q) => {
358                    out.put(b"\"")?;
359                    out.put(q.raw())?;
360                    out.put(b"\"")?;
361                }
362            }
363        }
364        Ok(())
365    }
366}
367
368/// The length of a word written as it was sent.
369fn raw_word_len(w: &Word<'_>) -> usize {
370    match w {
371        Word::Token(t) => t.len(),
372        Word::Quoted(q) => q.raw().len() + 2,
373    }
374}
375
376/// Writes `s` in ASCII lowercase, through a small stack buffer.
377fn put_lower(s: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
378    let mut buf = [0u8; 64];
379    for chunk in s.chunks(buf.len()) {
380        let dst = buf.get_mut(..chunk.len()).unwrap_or(&mut []);
381        for (d, &c) in dst.iter_mut().zip(chunk) {
382            *d = c.to_ascii_lowercase();
383        }
384        out.put(dst)?;
385    }
386    Ok(())
387}
388
389/// A validated multipart boundary, unquoted.
390///
391/// # Examples
392///
393/// ```
394/// let mt = parse_net::parse_media_type(b"multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")?;
395/// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"gc0p4Jq0M2Yt08j34c0p"[..]));
396/// # Ok::<(), parse_net::ParseError>(())
397/// ```
398#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
399pub struct Boundary<'a>(&'a [u8]);
400
401impl<'a> Boundary<'a> {
402    /// The boundary bytes.
403    #[must_use]
404    pub const fn as_bytes(&self) -> &'a [u8] {
405        self.0
406    }
407}
408
409#[cfg(test)]
410mod tests {
411    use std::{vec, vec::Vec};
412
413    use super::*;
414    use crate::write::SliceSink;
415
416    fn mt(s: &[u8]) -> Result<MediaType<'_>> {
417        parse_media_type(s)
418    }
419
420    #[test]
421    fn test_media_type_edge_cases() {
422        assert!(mt(b"text/html").unwrap().is("text", "html"));
423        assert!(mt(b"TEXT/HTML").unwrap().essence_is("text/html"));
424        assert!(mt(b"text/html;").unwrap().params().is_empty());
425        assert!(mt(b"text/html ;charset=utf-8").unwrap().charset().is_some());
426        assert!(mt(b" text/html \t").is_ok());
427        let e = mt(b"text/ html").unwrap_err();
428        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 5));
429        let e = mt(b"text /html").unwrap_err();
430        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 4));
431        assert_eq!(
432            mt(b"text/").unwrap_err().kind(),
433            ErrorKind::InvalidMediaType
434        );
435        assert_eq!(
436            mt(b"/html").unwrap_err().kind(),
437            ErrorKind::InvalidMediaType
438        );
439        assert_eq!(mt(b"").unwrap_err().kind(), ErrorKind::InvalidMediaType);
440        assert_eq!(mt(b"text").unwrap_err().kind(), ErrorKind::InvalidMediaType);
441        assert_eq!(mt(b"*/*").unwrap_err().kind(), ErrorKind::Wildcard);
442        let e = mt(b"text/*").unwrap_err();
443        assert_eq!((e.kind(), e.offset()), (ErrorKind::Wildcard, 5));
444        assert_eq!(
445            mt(b"text/html x").unwrap_err().kind(),
446            ErrorKind::InvalidMediaType
447        );
448        assert_eq!(mt(b"text/html\r").unwrap_err().kind(), ErrorKind::BareCr);
449        assert_eq!(
450            mt(b"text/html;a=b\nc").unwrap_err().kind(),
451            ErrorKind::LineFeed
452        );
453        let e = mt(b"text/html; charset=a; Charset=b").unwrap_err();
454        assert_eq!((e.kind(), e.offset()), (ErrorKind::DuplicateParameter, 22));
455    }
456
457    #[test]
458    fn test_media_type_suffix_and_predicates() {
459        let v = mt(b"application/vnd.api+json").unwrap();
460        assert_eq!(v.suffix(), Some(&b"json"[..]));
461        assert!(v.is_json());
462        assert!(mt(b"application/json").unwrap().is_json());
463        assert!(!mt(b"application/jsonx").unwrap().is_json());
464        assert_eq!(mt(b"application/a+").unwrap().suffix(), None);
465        assert!(
466            mt(b"application/x-www-form-urlencoded")
467                .unwrap()
468                .is_form_urlencoded()
469        );
470        assert!(!mt(b"text/html").unwrap().essence_is("texthtml"));
471    }
472
473    fn boundary(s: &[u8]) -> Result<Option<Vec<u8>>> {
474        Ok(mt(s)?.boundary()?.map(|b| b.as_bytes().to_vec()))
475    }
476
477    #[test]
478    fn test_boundary_rfc2046_rules() {
479        assert_eq!(boundary(b"multipart/form-data").unwrap(), None);
480        assert_eq!(
481            boundary(b"multipart/form-data; boundary=")
482                .unwrap_err()
483                .kind(),
484            ErrorKind::MissingValue
485        );
486        assert_eq!(
487            boundary(b"multipart/form-data; boundary=\"\"")
488                .unwrap_err()
489                .kind(),
490            ErrorKind::InvalidBoundary
491        );
492        let seventy = vec![b'a'; 70];
493        let mut v = b"multipart/mixed; boundary=".to_vec();
494        v.extend_from_slice(&seventy);
495        assert_eq!(boundary(&v).unwrap(), Some(seventy));
496        v.push(b'a');
497        assert_eq!(boundary(&v).unwrap_err().kind(), ErrorKind::InvalidBoundary);
498        assert_eq!(
499            boundary(b"multipart/mixed; boundary=\"ab \"")
500                .unwrap_err()
501                .kind(),
502            ErrorKind::InvalidBoundary
503        );
504        assert_eq!(
505            boundary(b"multipart/mixed; boundary=\"a b\"").unwrap(),
506            Some(b"a b".to_vec())
507        );
508        assert_eq!(
509            boundary(b"multipart/mixed; boundary=\"a\\\\b\"")
510                .unwrap_err()
511                .kind(),
512            ErrorKind::InvalidBoundary
513        );
514        assert_eq!(
515            boundary(b"multipart/mixed; boundary=a*b")
516                .unwrap_err()
517                .kind(),
518            ErrorKind::InvalidBoundary
519        );
520        assert_eq!(
521            mt(b"multipart/mixed; boundary=a; boundary=b")
522                .unwrap_err()
523                .kind(),
524            ErrorKind::DuplicateParameter
525        );
526    }
527
528    #[test]
529    fn test_media_write_refuses_bad_names_and_round_trips() {
530        let mut buf = [0u8; 128];
531        let mut out = SliceSink::new(&mut buf);
532        assert_eq!(write("text", "*", &[], &mut out), Err(WriteError::NotToken));
533        assert_eq!(
534            write("te xt", "a", &[], &mut out),
535            Err(WriteError::NotToken)
536        );
537        assert_eq!(
538            write("a", "b", &[("x y", "1")], &mut out),
539            Err(WriteError::NotToken)
540        );
541        assert_eq!(
542            write("a", "b", &[("x", "1\r\n")], &mut out),
543            Err(WriteError::Forbidden { at: 1 })
544        );
545        assert!(out.is_empty());
546        write("a", "b", &[("x", "1 2"), ("y", "")], &mut out).unwrap();
547        let back = mt(out.written()).unwrap();
548        assert!(back.param("x").unwrap().eq_bytes(b"1 2"));
549        assert!(back.param("y").unwrap().is_empty());
550    }
551
552    #[test]
553    fn test_write_canonical_lowercases_names_only() {
554        let v = mt(b"TEXT/Plain;  FORMAT=Flowed;x=\"A\\\"B\"").unwrap();
555        let mut buf = [0u8; 64];
556        let mut out = SliceSink::new(&mut buf);
557        v.write_canonical(&mut out).unwrap();
558        assert_eq!(out.written(), b"text/plain; format=Flowed; x=\"A\\\"B\"");
559        let mut small = [0u8; 4];
560        let mut out = SliceSink::new(&mut small);
561        assert_eq!(v.write_canonical(&mut out), Err(WriteError::Full));
562        assert!(out.is_empty());
563    }
564}