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    #[must_use]
220    pub const fn type_(&self) -> &'a [u8] {
221        self.type_
222    }
223
224    /// The subtype, or `*`.
225    #[must_use]
226    pub const fn subtype(&self) -> &'a [u8] {
227        self.subtype
228    }
229
230    /// Whether this range matches `offer`: the type and subtype match (or
231    /// are `*`), and every parameter of the range is present in the offer
232    /// with an equal value (case-sensitive, except `charset`).
233    #[must_use]
234    pub fn matches(&self, offer: &MediaType<'_>) -> bool {
235        let type_ok = self.type_ == b"*" || self.type_.eq_ignore_ascii_case(offer.type_());
236        let sub_ok = self.subtype == b"*" || self.subtype.eq_ignore_ascii_case(offer.subtype());
237        type_ok
238            && sub_ok
239            && self.params.iter().all(|p| {
240                let charset = p.name.eq_ignore_ascii_case(b"charset");
241                offer.params().iter().any(|o| {
242                    o.name.eq_ignore_ascii_case(p.name) && p.value.same_value(&o.value, charset)
243                })
244            })
245    }
246
247    /// How specific the range is: 4 for a type with parameters, 3 for an
248    /// exact type, 2 for `type/*`, 1 for `*/*`.
249    #[must_use]
250    pub const fn specificity(&self) -> u8 {
251        if matches!(self.type_, [b'*']) {
252            1
253        } else if matches!(self.subtype, [b'*']) {
254            2
255        } else if self.params.is_empty() {
256            3
257        } else {
258            4
259        }
260    }
261
262    /// The weight (`q`), 1 when absent.
263    #[must_use]
264    pub const fn weight(&self) -> QValue {
265        self.weight
266    }
267
268    /// The media-type parameters: those before `q`.
269    #[must_use]
270    pub const fn params(&self) -> Params<'a> {
271        self.params
272    }
273}
274
275/// Writes `type/subtype` and then `; name=value` for each pair, values as a
276/// token when they are one and as a quoted string otherwise.
277///
278/// # Errors
279///
280/// [`WriteError::NotToken`] for a type, subtype, or name that is not a token
281/// (or is `*`); [`WriteError::DuplicateName`] for two parameters with the
282/// same name (ASCII case-insensitive), which parsers refuse;
283/// [`WriteError::Forbidden`] for a control byte in a value;
284/// [`WriteError::Full`]. Nothing is written on error when the sink reports
285/// its room.
286///
287/// # Examples
288///
289/// ```
290/// use parse_net::{media, SliceSink};
291///
292/// let mut buf = [0u8; 64];
293/// let mut out = SliceSink::new(&mut buf);
294/// media::write("multipart", "form-data", &[("boundary", "a b")], &mut out)?;
295/// assert_eq!(out.written(), b"multipart/form-data; boundary=\"a b\"");
296/// # Ok::<(), parse_net::WriteError>(())
297/// ```
298pub fn write(
299    type_: &str,
300    subtype: &str,
301    params: &[(&str, &str)],
302    out: &mut impl Sink,
303) -> core::result::Result<(), WriteError> {
304    let (t, s) = (type_.as_bytes(), subtype.as_bytes());
305    if !is_token(t) || !is_token(s) || t == b"*" || s == b"*" {
306        return Err(WriteError::NotToken);
307    }
308    unique_names(params)?;
309    let mut len = t.len().saturating_add(1).saturating_add(s.len());
310    for (name, value) in params {
311        if !is_token(name.as_bytes()) {
312            return Err(WriteError::NotToken);
313        }
314        let v = word_len(value.as_bytes())?;
315        len = len
316            .saturating_add(3)
317            .saturating_add(name.len())
318            .saturating_add(v);
319    }
320    reserve(out, len)?;
321    out.put(t)?;
322    out.put(b"/")?;
323    out.put(s)?;
324    for (name, value) in params {
325        out.put(b"; ")?;
326        out.put(name.as_bytes())?;
327        out.put(b"=")?;
328        write_word(value.as_bytes(), out)?;
329    }
330    Ok(())
331}
332
333/// A validated media type.
334///
335/// # Examples
336///
337/// ```
338/// let ld = parse_net::parse_media_type(b"application/ld+json")?;
339/// assert_eq!(ld.suffix(), Some(&b"json"[..]));
340/// assert!(ld.is_json());
341/// # Ok::<(), parse_net::ParseError>(())
342/// ```
343#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
344pub struct MediaType<'a> {
345    type_: &'a [u8],
346    subtype: &'a [u8],
347    params: Params<'a>,
348}
349
350impl<'a> MediaType<'a> {
351    /// The type, as sent.
352    #[must_use]
353    pub const fn type_(&self) -> &'a [u8] {
354        self.type_
355    }
356
357    /// The subtype, as sent.
358    #[must_use]
359    pub const fn subtype(&self) -> &'a [u8] {
360        self.subtype
361    }
362
363    /// The structured syntax suffix: the part of the subtype after its last
364    /// `+` (`json` of `ld+json`), when there is one.
365    #[must_use]
366    pub fn suffix(&self) -> Option<&'a [u8]> {
367        let plus = self.subtype.iter().rposition(|&b| b == b'+')?;
368        self.subtype.get(plus + 1..).filter(|s| !s.is_empty())
369    }
370
371    /// Whether `type/subtype` equals `essence`, ignoring ASCII case.
372    #[must_use]
373    pub fn essence_is(&self, essence: &str) -> bool {
374        match essence.split_once('/') {
375            Some((t, s)) => self.is(t, s),
376            None => false,
377        }
378    }
379
380    /// Whether the type and subtype equal `type_` and `subtype`, ignoring
381    /// ASCII case.
382    #[must_use]
383    pub fn is(&self, type_: &str, subtype: &str) -> bool {
384        self.type_.eq_ignore_ascii_case(type_.as_bytes())
385            && self.subtype.eq_ignore_ascii_case(subtype.as_bytes())
386    }
387
388    /// The parameters.
389    #[must_use]
390    pub const fn params(&self) -> Params<'a> {
391        self.params
392    }
393
394    /// The value of the parameter `name` (ASCII case-insensitive).
395    #[must_use]
396    pub fn param(&self, name: &str) -> Option<Word<'a>> {
397        self.params.get(name)
398    }
399
400    /// The `charset` parameter. Compare it case-insensitively.
401    #[must_use]
402    pub fn charset(&self) -> Option<Word<'a>> {
403        self.params.get("charset")
404    }
405
406    /// The `boundary` parameter, validated by RFC 2046 section 5.1.1: 1 to 70
407    /// `bchars`, the last not a space. `Ok(None)` when absent.
408    ///
409    /// # Errors
410    ///
411    /// [`ErrorKind::InvalidBoundary`], with the offset inside the boundary
412    /// value.
413    pub fn boundary(&self) -> Result<Option<Boundary<'a>>> {
414        let Some(word) = self.params.get("boundary") else {
415            return Ok(None);
416        };
417        let bytes = match word {
418            Word::Token(t) => t,
419            // `\` is not a bchar, so a quoted boundary cannot carry escapes.
420            Word::Quoted(q) if q.has_escapes() => {
421                let at = q.raw().iter().position(|&b| b == b'\\').unwrap_or(0);
422                return err(ErrorKind::InvalidBoundary, at);
423            }
424            Word::Quoted(q) => q.raw(),
425        };
426        if bytes.is_empty() {
427            return err(ErrorKind::InvalidBoundary, 0);
428        }
429        if let Some(at) = bytes
430            .iter()
431            .position(|&b| !(class::is_bchar_nospace(b) || b == b' '))
432        {
433            return err(ErrorKind::InvalidBoundary, at);
434        }
435        if bytes.len() > 70 {
436            return err(ErrorKind::InvalidBoundary, 70);
437        }
438        if bytes.last() == Some(&b' ') {
439            return err(ErrorKind::InvalidBoundary, bytes.len() - 1);
440        }
441        Ok(Some(Boundary(bytes)))
442    }
443
444    /// Whether this is `application/json` or has the `+json` suffix.
445    #[must_use]
446    pub fn is_json(&self) -> bool {
447        self.is("application", "json")
448            || self
449                .suffix()
450                .is_some_and(|s| s.eq_ignore_ascii_case(b"json"))
451    }
452
453    /// Whether this is `application/x-www-form-urlencoded`.
454    #[must_use]
455    pub fn is_form_urlencoded(&self) -> bool {
456        self.is("application", "x-www-form-urlencoded")
457    }
458
459    /// Whether this is `multipart/form-data`.
460    #[must_use]
461    pub fn is_multipart_form_data(&self) -> bool {
462        self.is("multipart", "form-data")
463    }
464
465    /// Writes the media type with the type, subtype, and parameter names in
466    /// lowercase and the values as sent, separated by `; `.
467    ///
468    /// # Errors
469    ///
470    /// [`WriteError::Full`]; nothing is written when the sink reports its
471    /// room.
472    ///
473    /// # Examples
474    ///
475    /// ```
476    /// use parse_net::{parse_media_type, SliceSink};
477    ///
478    /// let mt = parse_media_type(b"Text/HTML;Charset=\"UTF-8\"")?;
479    /// let mut buf = [0u8; 64];
480    /// let mut out = SliceSink::new(&mut buf);
481    /// mt.write_canonical(&mut out).expect("room");
482    /// assert_eq!(out.written(), b"text/html; charset=\"UTF-8\"");
483    /// # Ok::<(), parse_net::ParseError>(())
484    /// ```
485    pub fn write_canonical(&self, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
486        let mut len = self.type_.len() + 1 + self.subtype.len();
487        for p in self.params.iter() {
488            len = len.saturating_add(3 + p.name.len() + raw_word_len(&p.value));
489        }
490        reserve(out, len)?;
491        put_lower(self.type_, out)?;
492        out.put(b"/")?;
493        put_lower(self.subtype, out)?;
494        for p in self.params.iter() {
495            out.put(b"; ")?;
496            put_lower(p.name, out)?;
497            out.put(b"=")?;
498            match p.value {
499                Word::Token(t) => out.put(t)?,
500                Word::Quoted(q) => {
501                    out.put(b"\"")?;
502                    out.put(q.raw())?;
503                    out.put(b"\"")?;
504                }
505            }
506        }
507        Ok(())
508    }
509}
510
511/// The length of a word written as it was sent.
512fn raw_word_len(w: &Word<'_>) -> usize {
513    match w {
514        Word::Token(t) => t.len(),
515        Word::Quoted(q) => q.raw().len() + 2,
516    }
517}
518
519/// Writes `s` in ASCII lowercase, through a small stack buffer.
520fn put_lower(s: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
521    let mut buf = [0u8; 64];
522    for chunk in s.chunks(buf.len()) {
523        let dst = buf.get_mut(..chunk.len()).unwrap_or(&mut []);
524        for (d, &c) in dst.iter_mut().zip(chunk) {
525            *d = c.to_ascii_lowercase();
526        }
527        out.put(dst)?;
528    }
529    Ok(())
530}
531
532/// A validated multipart boundary, unquoted.
533///
534/// # Examples
535///
536/// ```
537/// let mt = parse_net::parse_media_type(b"multipart/mixed; boundary=gc0p4Jq0M2Yt08j34c0p")?;
538/// assert_eq!(mt.boundary()?.map(|b| b.as_bytes()), Some(&b"gc0p4Jq0M2Yt08j34c0p"[..]));
539/// # Ok::<(), parse_net::ParseError>(())
540/// ```
541#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
542pub struct Boundary<'a>(&'a [u8]);
543
544impl<'a> Boundary<'a> {
545    /// The boundary bytes.
546    #[must_use]
547    pub const fn as_bytes(&self) -> &'a [u8] {
548        self.0
549    }
550}
551
552#[cfg(test)]
553mod tests {
554    use std::{vec, vec::Vec};
555
556    use super::*;
557    use crate::write::SliceSink;
558
559    fn mt(s: &[u8]) -> Result<MediaType<'_>> {
560        parse_media_type(s)
561    }
562
563    #[test]
564    fn test_media_type_edge_cases() {
565        assert!(mt(b"text/html").unwrap().is("text", "html"));
566        assert!(mt(b"TEXT/HTML").unwrap().essence_is("text/html"));
567        assert!(mt(b"text/html;").unwrap().params().is_empty());
568        assert!(mt(b"text/html ;charset=utf-8").unwrap().charset().is_some());
569        assert!(mt(b" text/html \t").is_ok());
570        let e = mt(b"text/ html").unwrap_err();
571        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 5));
572        let e = mt(b"text /html").unwrap_err();
573        assert_eq!((e.kind(), e.offset()), (ErrorKind::InvalidMediaType, 4));
574        assert_eq!(
575            mt(b"text/").unwrap_err().kind(),
576            ErrorKind::InvalidMediaType
577        );
578        assert_eq!(
579            mt(b"/html").unwrap_err().kind(),
580            ErrorKind::InvalidMediaType
581        );
582        assert_eq!(mt(b"").unwrap_err().kind(), ErrorKind::InvalidMediaType);
583        assert_eq!(mt(b"text").unwrap_err().kind(), ErrorKind::InvalidMediaType);
584        assert_eq!(mt(b"*/*").unwrap_err().kind(), ErrorKind::Wildcard);
585        let e = mt(b"text/*").unwrap_err();
586        assert_eq!((e.kind(), e.offset()), (ErrorKind::Wildcard, 5));
587        assert_eq!(
588            mt(b"text/html x").unwrap_err().kind(),
589            ErrorKind::InvalidMediaType
590        );
591        assert_eq!(mt(b"text/html\r").unwrap_err().kind(), ErrorKind::BareCr);
592        assert_eq!(
593            mt(b"text/html;a=b\nc").unwrap_err().kind(),
594            ErrorKind::LineFeed
595        );
596        let e = mt(b"text/html; charset=a; Charset=b").unwrap_err();
597        assert_eq!((e.kind(), e.offset()), (ErrorKind::DuplicateParameter, 22));
598    }
599
600    #[test]
601    fn test_media_type_suffix_and_predicates() {
602        let v = mt(b"application/vnd.api+json").unwrap();
603        assert_eq!(v.suffix(), Some(&b"json"[..]));
604        assert!(v.is_json());
605        assert!(mt(b"application/json").unwrap().is_json());
606        assert!(!mt(b"application/jsonx").unwrap().is_json());
607        assert_eq!(mt(b"application/a+").unwrap().suffix(), None);
608        assert!(
609            mt(b"application/x-www-form-urlencoded")
610                .unwrap()
611                .is_form_urlencoded()
612        );
613        assert!(!mt(b"text/html").unwrap().essence_is("texthtml"));
614    }
615
616    fn boundary(s: &[u8]) -> Result<Option<Vec<u8>>> {
617        Ok(mt(s)?.boundary()?.map(|b| b.as_bytes().to_vec()))
618    }
619
620    #[test]
621    fn test_boundary_rfc2046_rules() {
622        assert_eq!(boundary(b"multipart/form-data").unwrap(), None);
623        assert_eq!(
624            boundary(b"multipart/form-data; boundary=")
625                .unwrap_err()
626                .kind(),
627            ErrorKind::MissingValue
628        );
629        assert_eq!(
630            boundary(b"multipart/form-data; boundary=\"\"")
631                .unwrap_err()
632                .kind(),
633            ErrorKind::InvalidBoundary
634        );
635        let seventy = vec![b'a'; 70];
636        let mut v = b"multipart/mixed; boundary=".to_vec();
637        v.extend_from_slice(&seventy);
638        assert_eq!(boundary(&v).unwrap(), Some(seventy));
639        v.push(b'a');
640        assert_eq!(boundary(&v).unwrap_err().kind(), ErrorKind::InvalidBoundary);
641        assert_eq!(
642            boundary(b"multipart/mixed; boundary=\"ab \"")
643                .unwrap_err()
644                .kind(),
645            ErrorKind::InvalidBoundary
646        );
647        assert_eq!(
648            boundary(b"multipart/mixed; boundary=\"a b\"").unwrap(),
649            Some(b"a b".to_vec())
650        );
651        assert_eq!(
652            boundary(b"multipart/mixed; boundary=\"a\\\\b\"")
653                .unwrap_err()
654                .kind(),
655            ErrorKind::InvalidBoundary
656        );
657        assert_eq!(
658            boundary(b"multipart/mixed; boundary=a*b")
659                .unwrap_err()
660                .kind(),
661            ErrorKind::InvalidBoundary
662        );
663        assert_eq!(
664            mt(b"multipart/mixed; boundary=a; boundary=b")
665                .unwrap_err()
666                .kind(),
667            ErrorKind::DuplicateParameter
668        );
669    }
670
671    #[test]
672    fn test_media_write_refuses_bad_names_and_round_trips() {
673        let mut buf = [0u8; 128];
674        let mut out = SliceSink::new(&mut buf);
675        assert_eq!(write("text", "*", &[], &mut out), Err(WriteError::NotToken));
676        assert_eq!(
677            write("te xt", "a", &[], &mut out),
678            Err(WriteError::NotToken)
679        );
680        assert_eq!(
681            write("a", "b", &[("x y", "1")], &mut out),
682            Err(WriteError::NotToken)
683        );
684        assert_eq!(
685            write("a", "b", &[("x", "1\r\n")], &mut out),
686            Err(WriteError::Forbidden { at: 1 })
687        );
688        assert_eq!(
689            write("a", "b", &[("x", "1"), ("X", "2")], &mut out),
690            Err(WriteError::DuplicateName)
691        );
692        assert!(out.is_empty());
693        write("a", "b", &[("x", "1 2"), ("y", "")], &mut out).unwrap();
694        let back = mt(out.written()).unwrap();
695        assert!(back.param("x").unwrap().eq_bytes(b"1 2"));
696        assert!(back.param("y").unwrap().is_empty());
697    }
698
699    #[test]
700    fn test_write_canonical_lowercases_names_only() {
701        let v = mt(b"TEXT/Plain;  FORMAT=Flowed;x=\"A\\\"B\"").unwrap();
702        let mut buf = [0u8; 64];
703        let mut out = SliceSink::new(&mut buf);
704        v.write_canonical(&mut out).unwrap();
705        assert_eq!(out.written(), b"text/plain; format=Flowed; x=\"A\\\"B\"");
706        let mut small = [0u8; 4];
707        let mut out = SliceSink::new(&mut small);
708        assert_eq!(v.write_canonical(&mut out), Err(WriteError::Full));
709        assert!(out.is_empty());
710    }
711}