Skip to main content

parse_net/mail/
params.rs

1//! RFC 2045 section 5.1 parameters with RFC 2231 continuations and
2//! extended values.
3
4use super::cfws;
5use crate::{
6    class,
7    error::{ErrorKind, ParseError, Result, WriteError, err},
8    ext_value::{Charset, ExtValue},
9    limits::{Limits, MAX_RFC2231_SECTIONS},
10    scan::control_kind,
11    text::{Word, parse_quoted},
12    write::{CountSink, Sink, reserve},
13};
14
15/// Limits for walking text that was already validated.
16const RELAXED: Limits = Limits::DEFAULT
17    .max_params(u16::MAX)
18    .max_comment_depth(u8::MAX);
19
20/// How a parameter name takes part in RFC 2231.
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
22enum Section {
23    /// `name`.
24    Plain,
25    /// `name*`: one extended value.
26    Ext,
27    /// `name*N` or `name*N*`.
28    Part { n: usize, ext: bool },
29}
30
31/// Splits `name*N*` into the base name and its section.
32fn split_name(name: &[u8]) -> (&[u8], Section) {
33    let (body, ext) = match name.split_last() {
34        Some((b'*', body)) => (body, true),
35        _ => (name, false),
36    };
37    if let Some(star) = body.iter().rposition(|&b| b == b'*') {
38        let digits = body.get(star + 1..).unwrap_or(&[]);
39        let canonical = !digits.is_empty()
40            && digits.len() <= 3
41            && digits.iter().all(u8::is_ascii_digit)
42            && (digits.len() == 1 || digits.first() != Some(&b'0'));
43        if canonical {
44            let n = digits
45                .iter()
46                .fold(0usize, |a, &d| a * 10 + usize::from(d - b'0'));
47            return (body.get(..star).unwrap_or(&[]), Section::Part { n, ext });
48        }
49    }
50    if ext {
51        (body, Section::Ext)
52    } else {
53        (name, Section::Plain)
54    }
55}
56
57fn refused(v: &[u8], at: usize, kind: ErrorKind) -> ParseError {
58    match v.get(at) {
59        Some(&b) if crate::scan::is_control(b) => ParseError::new(control_kind(b), at),
60        _ => ParseError::new(kind, at),
61    }
62}
63
64/// One `; attribute = value` at `v[i]` (the `;`); returns the name, value,
65/// where the value starts, and the index after it.
66fn param_at<'a>(
67    v: &'a [u8],
68    i: usize,
69    limits: &Limits,
70) -> Result<(&'a [u8], Word<'a>, usize, usize)> {
71    let j = cfws(v, i + 1, limits)?;
72    let n = v
73        .get(j..)
74        .unwrap_or(&[])
75        .iter()
76        .take_while(|&&b| class::is_mime_token_char(b))
77        .count();
78    if n == 0 {
79        return Err(refused(v, j, ErrorKind::NotToken));
80    }
81    let name = v.get(j..j + n).unwrap_or(&[]);
82    let k = cfws(v, j + n, limits)?;
83    if v.get(k) != Some(&b'=') {
84        return Err(refused(v, k, ErrorKind::MissingValue));
85    }
86    let at = cfws(v, k + 1, limits)?;
87    let rest = v.get(at..).unwrap_or(&[]);
88    if rest.first() == Some(&b'"') {
89        let (q, used) = parse_quoted(rest).map_err(|e| e.shifted(at))?;
90        return Ok((name, Word::Quoted(q), at, at + used));
91    }
92    let t = rest
93        .iter()
94        .take_while(|&&b| class::is_mime_token_char(b))
95        .count();
96    if t == 0 {
97        return Err(refused(v, at, ErrorKind::MissingValue));
98    }
99    Ok((name, Word::Token(rest.get(..t).unwrap_or(&[])), at, at + t))
100}
101
102/// Whether `t` is RFC 2231 `extended-other-values`: attr-chars and `%XX`.
103fn pct_chars(t: &[u8]) -> Option<usize> {
104    let mut i = 0usize;
105    while let Some(&b) = t.get(i) {
106        if b == b'%' {
107            match (t.get(i + 1), t.get(i + 2)) {
108                (Some(&h), Some(&l)) if class::is_hexdig(h) && class::is_hexdig(l) => i += 3,
109                _ => return Some(i),
110            }
111        } else if class::is_attr_char(b) {
112            i += 1;
113        } else {
114            return Some(i);
115        }
116    }
117    None
118}
119
120/// Parses `*( ";" attribute "=" value )` (RFC 2045 section 5.1) with the MIME
121/// token class, CFWS between the parts, and case-insensitive names, and
122/// validates every RFC 2231 extended value and continuation. Returns the
123/// parameters and the bytes used (parsing stops at the first byte that does
124/// not start another parameter).
125///
126/// # Errors
127///
128/// [`ErrorKind::NotToken`], [`ErrorKind::MissingValue`],
129/// [`ErrorKind::DuplicateParameter`], [`ErrorKind::UnterminatedQuote`],
130/// [`ErrorKind::TooManyMembers`] beyond `max_params`; for RFC 2231:
131/// [`ErrorKind::SectionGap`], [`ErrorKind::TooManySections`] beyond
132/// `max_rfc2231_sections`, the [`ExtValue::parse`] errors (a charset outside
133/// section 0 is [`ErrorKind::InvalidPercentEncoding`] at its `'`),
134/// [`ErrorKind::InvalidUtf8`], [`ErrorKind::ControlInDecoded`];
135/// [`ErrorKind::UnbalancedComment`], [`ErrorKind::CommentTooDeep`].
136///
137/// # Examples
138///
139/// ```
140/// use parse_net::{mail, Limits};
141///
142/// let (p, used) = mail::parse_mime_params(b"; charset=\"utf-8\" (comment); format=flowed rest", &Limits::DEFAULT)?;
143/// assert_eq!((p.len(), used), (2, 42));
144/// let mut buf = [0u8; 8];
145/// assert_eq!(p.decode_into("CHARSET", &mut buf)?, Some("utf-8"));
146/// # Ok::<(), parse_net::ParseError>(())
147/// ```
148pub fn parse_mime_params<'a>(s: &'a [u8], limits: &Limits) -> Result<(MimeParams<'a>, usize)> {
149    let (mut i, mut count) = (0usize, 0usize);
150    loop {
151        let j = cfws(s, i, limits)?;
152        if s.get(j) != Some(&b';') {
153            break;
154        }
155        let (name, value, at, end) = param_at(s, j, limits)?;
156        count += 1;
157        if count > usize::from(limits.params) {
158            return err(ErrorKind::TooManyMembers, j);
159        }
160        let earlier = MimeParams {
161            text: s.get(..j).unwrap_or(&[]),
162            count: count - 1,
163        };
164        if earlier.iter().any(|(n, _)| n.eq_ignore_ascii_case(name)) {
165            return err(ErrorKind::DuplicateParameter, j);
166        }
167        match (split_name(name).1, value) {
168            (Section::Ext | Section::Part { n: 0, ext: true }, Word::Token(t)) => {
169                let ext = ExtValue::parse(t).map_err(|e| e.shifted(at))?;
170                if split_name(name).1 == Section::Ext {
171                    ext.check_text()
172                        .map_err(|e| ParseError::new(e.kind(), at))?;
173                }
174            }
175            (Section::Part { ext: true, .. }, Word::Token(t)) => {
176                if let Some(bad) = pct_chars(t) {
177                    return err(ErrorKind::InvalidPercentEncoding, at + bad);
178                }
179            }
180            (Section::Ext | Section::Part { ext: true, .. }, Word::Quoted(_)) => {
181                // Extended values are tokens (RFC 2231 section 7).
182                return err(ErrorKind::InvalidCharset, at);
183            }
184            _ => {}
185        }
186        i = end;
187    }
188    let params = MimeParams {
189        text: s.get(..i).unwrap_or(&[]),
190        count,
191    };
192    // Every continuation must be whole and decodable.
193    for (name, _) in params.iter() {
194        if let (base, Section::Part { n: 0, .. }) = split_name(name) {
195            let c = Continuation { params, name: base };
196            let _ = c.walk(limits, &mut |_| {})?;
197        } else if let (base, Section::Part { .. }) = split_name(name) {
198            let has_zero = params
199                .iter()
200                .any(|(n, _)| matches!(split_name(n), (b, Section::Part { n: 0, .. }) if b.eq_ignore_ascii_case(base)));
201            if !has_zero {
202                return err(ErrorKind::SectionGap, 0);
203            }
204        }
205    }
206    Ok((params, i))
207}
208
209/// Validated MIME parameters.
210///
211/// # Examples
212///
213/// ```
214/// use parse_net::{mail, Limits};
215///
216/// let (p, _) = mail::parse_mime_params(b"; charset=us-ascii", &Limits::DEFAULT)?;
217/// let mut buf = [0u8; 16];
218/// assert_eq!(p.decode_into("charset", &mut buf)?, Some("us-ascii"));
219/// # Ok::<(), parse_net::ParseError>(())
220/// ```
221#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
222pub struct MimeParams<'a> {
223    text: &'a [u8],
224    count: usize,
225}
226
227impl<'a> MimeParams<'a> {
228    /// The value of `name` (case-insensitive): a continuation when
229    /// `name*0` or `name*0*` is present, else the extended `name*`, else the
230    /// plain `name`.
231    ///
232    /// # Examples
233    ///
234    /// ```
235    /// use parse_net::{mail::{self, MimeValue}, Limits};
236    ///
237    /// let (p, _) = mail::parse_mime_params(b"; name=plain.txt; name*=UTF-8''ext.txt", &Limits::DEFAULT)?;
238    /// assert!(matches!(p.get("NAME"), Some(MimeValue::Extended(e)) if e.encoded() == b"ext.txt"));
239    /// assert!(p.get("other").is_none());
240    /// # Ok::<(), parse_net::ParseError>(())
241    /// ```
242    #[must_use]
243    pub fn get(&self, name: &str) -> Option<MimeValue<'a>> {
244        let want = name.as_bytes();
245        let mut ext = None;
246        let mut plain = None;
247        for (n, w) in self.iter() {
248            match split_name(n) {
249                (b, Section::Part { n: 0, .. }) if b.eq_ignore_ascii_case(want) => {
250                    return Some(MimeValue::Continued(Continuation {
251                        params: *self,
252                        name: b,
253                    }));
254                }
255                (b, Section::Ext) if b.eq_ignore_ascii_case(want) => {
256                    if let Word::Token(t) = w {
257                        ext = ExtValue::parse(t).ok();
258                    }
259                }
260                (b, Section::Plain) if b.eq_ignore_ascii_case(want) => plain = Some(w),
261                _ => {}
262            }
263        }
264        ext.map(MimeValue::Extended).or(plain.map(MimeValue::Plain))
265    }
266
267    /// Decodes the value of `name` into `out` (as [`get`](Self::get)
268    /// chooses it); `Ok(None)` when absent.
269    ///
270    /// # Errors
271    ///
272    /// [`ErrorKind::BufferTooSmall`]; [`ErrorKind::InvalidUtf8`] for a plain
273    /// value that is not UTF-8. Nothing is written on error.
274    ///
275    /// # Examples
276    ///
277    /// ```
278    /// use parse_net::{mail, ErrorKind, Limits};
279    ///
280    /// let (p, _) = mail::parse_mime_params(b"; boundary=\"simple boundary\"", &Limits::DEFAULT)?;
281    /// let mut buf = [0u8; 32];
282    /// assert_eq!(p.decode_into("boundary", &mut buf)?, Some("simple boundary"));
283    /// assert_eq!(p.decode_into("charset", &mut buf)?, None);
284    /// assert_eq!(p.decode_into("boundary", &mut [0u8; 4]).unwrap_err().kind(), ErrorKind::BufferTooSmall);
285    /// # Ok::<(), parse_net::ParseError>(())
286    /// ```
287    pub fn decode_into<'b>(&self, name: &str, out: &'b mut [u8]) -> Result<Option<&'b str>> {
288        match self.get(name) {
289            None => Ok(None),
290            Some(MimeValue::Continued(c)) => c.decode_into(out).map(Some),
291            Some(MimeValue::Extended(e)) => e.decode_into(out).map(Some),
292            Some(MimeValue::Plain(w)) => {
293                let len = w.len();
294                let Some(dst) = out.get_mut(..len) else {
295                    return err(ErrorKind::BufferTooSmall, 0);
296                };
297                let utf8 = core::str::from_utf8(w.unescape_into(dst)?).is_ok();
298                if !utf8 {
299                    dst.fill(0);
300                    return err(ErrorKind::InvalidUtf8, 0);
301                }
302                core::str::from_utf8(dst)
303                    .map(Some)
304                    .map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
305            }
306        }
307    }
308
309    /// The parameters as sent, sections included, in order.
310    ///
311    /// # Examples
312    ///
313    /// ```
314    /// use parse_net::{mail, Limits};
315    ///
316    /// let (p, _) = mail::parse_mime_params(b"; a*0=x; a*1=y", &Limits::DEFAULT)?;
317    /// let names: Vec<&[u8]> = p.iter().map(|(name, _)| name).collect();
318    /// assert_eq!(names, [&b"a*0"[..], b"a*1"]);
319    /// # Ok::<(), parse_net::ParseError>(())
320    /// ```
321    #[must_use]
322    pub fn iter(&self) -> MimeParamIter<'a> {
323        MimeParamIter { s: self.text, i: 0 }
324    }
325
326    /// The number of parameters, sections counted one by one.
327    ///
328    /// # Examples
329    ///
330    /// ```
331    /// use parse_net::{mail, Limits};
332    ///
333    /// let (p, _) = mail::parse_mime_params(b"; a*0=x; a*1=y; b=z", &Limits::DEFAULT)?;
334    /// assert_eq!(p.len(), 3);
335    /// # Ok::<(), parse_net::ParseError>(())
336    /// ```
337    #[must_use]
338    pub const fn len(&self) -> usize {
339        self.count
340    }
341
342    /// Whether there are no parameters.
343    ///
344    /// # Examples
345    ///
346    /// ```
347    /// use parse_net::{mail, Limits};
348    ///
349    /// let (p, used) = mail::parse_mime_params(b" no params", &Limits::DEFAULT)?;
350    /// assert!(p.is_empty() && used == 0);
351    /// # Ok::<(), parse_net::ParseError>(())
352    /// ```
353    #[must_use]
354    pub const fn is_empty(&self) -> bool {
355        self.count == 0
356    }
357}
358
359/// The parameters of a validated [`MimeParams`].
360///
361/// # Examples
362///
363/// ```
364/// use parse_net::{mail, Limits};
365///
366/// let (p, _) = mail::parse_mime_params(b"; format=flowed", &Limits::DEFAULT)?;
367/// let mut it = p.iter();
368/// let (name, value) = it.next().expect("one parameter");
369/// assert!(name == b"format" && value.eq_bytes(b"flowed"));
370/// assert!(it.next().is_none());
371/// # Ok::<(), parse_net::ParseError>(())
372/// ```
373#[derive(Clone, Debug)]
374pub struct MimeParamIter<'a> {
375    s: &'a [u8],
376    i: usize,
377}
378
379impl<'a> Iterator for MimeParamIter<'a> {
380    type Item = (&'a [u8], Word<'a>);
381
382    fn next(&mut self) -> Option<Self::Item> {
383        let j = cfws(self.s, self.i, &RELAXED).ok()?;
384        if self.s.get(j) != Some(&b';') {
385            return None;
386        }
387        let (name, value, _, end) = param_at(self.s, j, &RELAXED).ok()?;
388        self.i = end;
389        Some((name, value))
390    }
391}
392
393/// A MIME parameter value.
394///
395/// # Examples
396///
397/// ```
398/// use parse_net::{mail::{self, MimeValue}, Limits};
399///
400/// let (p, _) = mail::parse_mime_params(b"; size=10; t*0=a; t*1=b", &Limits::DEFAULT)?;
401/// assert!(matches!(p.get("size"), Some(MimeValue::Plain(w)) if w.eq_bytes(b"10")));
402/// assert!(matches!(p.get("t"), Some(MimeValue::Continued(_))));
403/// # Ok::<(), parse_net::ParseError>(())
404/// ```
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
406pub enum MimeValue<'a> {
407    /// `name=value`.
408    Plain(Word<'a>),
409    /// `name*=charset'lang'value`.
410    Extended(ExtValue<'a>),
411    /// `name*0`, `name*1*`, ...: RFC 2231 continuations.
412    Continued(Continuation<'a>),
413}
414
415/// The sections of an RFC 2231 continued value, validated at parse.
416///
417/// # Examples
418///
419/// ```
420/// use parse_net::{mail::{self, MimeValue}, Limits};
421///
422/// let (p, _) = mail::parse_mime_params(b"; url*0=\"http://a.example/\"; url*1=\"b\"", &Limits::DEFAULT)?;
423/// let Some(MimeValue::Continued(c)) = p.get("url") else { unreachable!() };
424/// let mut buf = [0u8; 32];
425/// assert_eq!(c.decode_into(&mut buf)?, "http://a.example/b");
426/// # Ok::<(), parse_net::ParseError>(())
427/// ```
428#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
429pub struct Continuation<'a> {
430    params: MimeParams<'a>,
431    name: &'a [u8],
432}
433
434impl Continuation<'_> {
435    /// The number of sections.
436    ///
437    /// # Examples
438    ///
439    /// ```
440    /// use parse_net::{mail::{self, MimeValue}, Limits};
441    ///
442    /// let (p, _) = mail::parse_mime_params(b"; n*0=a; n*1=b; n*2=c", &Limits::DEFAULT)?;
443    /// let Some(MimeValue::Continued(c)) = p.get("n") else { unreachable!() };
444    /// assert_eq!(c.sections(), 3);
445    /// # Ok::<(), parse_net::ParseError>(())
446    /// ```
447    #[must_use]
448    pub fn sections(&self) -> usize {
449        self.params
450            .iter()
451            .filter(|(n, _)| matches!(split_name(n), (b, Section::Part { .. }) if b.eq_ignore_ascii_case(self.name)))
452            .count()
453    }
454
455    /// An upper bound on the decoded length (twice the raw length, for
456    /// ISO-8859-1 transcoded to UTF-8).
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// use parse_net::{mail::{self, MimeValue}, Limits};
462    ///
463    /// let (p, _) = mail::parse_mime_params(b"; t*0=abc; t*1=de", &Limits::DEFAULT)?;
464    /// let Some(MimeValue::Continued(c)) = p.get("t") else { unreachable!() };
465    /// assert_eq!(c.max_decoded_len(), 10);
466    /// let mut buf = [0u8; 10];
467    /// assert_eq!(c.decode_into(&mut buf)?, "abcde");
468    /// # Ok::<(), parse_net::ParseError>(())
469    /// ```
470    #[must_use]
471    pub fn max_decoded_len(&self) -> usize {
472        self.params
473            .iter()
474            .filter(|(n, _)| matches!(split_name(n), (b, Section::Part { .. }) if b.eq_ignore_ascii_case(self.name)))
475            .map(|(_, w)| w.len().saturating_mul(2))
476            .sum()
477    }
478
479    /// Decodes the sections in order into `out`: `%XX` in extended sections,
480    /// the charset of section 0 (UTF-8 or ISO-8859-1, transcoded).
481    ///
482    /// # Errors
483    ///
484    /// [`ErrorKind::BufferTooSmall`]; nothing is written on error.
485    ///
486    /// # Examples
487    ///
488    /// ```
489    /// use parse_net::{mail::{self, MimeValue}, Limits};
490    ///
491    /// let (p, _) = mail::parse_mime_params(b"; title*0*=iso-8859-1'en'%A3; title*1=\" rates\"", &Limits::DEFAULT)?;
492    /// let Some(MimeValue::Continued(c)) = p.get("title") else { unreachable!() };
493    /// let mut buf = [0u8; 16];
494    /// assert_eq!((c.sections(), c.decode_into(&mut buf)?), (2, "\u{a3} rates"));
495    /// # Ok::<(), parse_net::ParseError>(())
496    /// ```
497    pub fn decode_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
498        let len = self.walk(&RELAXED, &mut |_| {})?;
499        let Some(dst) = out.get_mut(..len) else {
500            return err(ErrorKind::BufferTooSmall, 0);
501        };
502        let mut n = 0usize;
503        let _ = self.walk(&RELAXED, &mut |b: u8| {
504            if let Some(d) = dst.get_mut(n) {
505                *d = b;
506            }
507            n += 1;
508        })?;
509        core::str::from_utf8(dst)
510            .map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
511    }
512
513    /// Orders the sections, checks them, and emits the decoded UTF-8 bytes;
514    /// returns their count.
515    fn walk(&self, limits: &Limits, emit: &mut impl FnMut(u8)) -> Result<usize> {
516        let mut slots: [Option<(bool, Word<'_>)>; MAX_RFC2231_SECTIONS as usize] =
517            [None; MAX_RFC2231_SECTIONS as usize];
518        let max = usize::from(limits.rfc2231_sections.min(MAX_RFC2231_SECTIONS));
519        let mut highest = 0usize;
520        for (name, w) in self.params.iter() {
521            if let (b, Section::Part { n, ext }) = split_name(name) {
522                if !b.eq_ignore_ascii_case(self.name) {
523                    continue;
524                }
525                if n >= max {
526                    return err(ErrorKind::TooManySections, 0);
527                }
528                if let Some(slot) = slots.get_mut(n) {
529                    *slot = Some((ext, w));
530                }
531                highest = highest.max(n);
532            }
533        }
534        let used = slots.get(..=highest).unwrap_or(&[]);
535        if used.iter().any(Option::is_none) {
536            return err(ErrorKind::SectionGap, 0);
537        }
538        let mut charset = Charset::Utf8;
539        let mut utf8 = crate::utf8::Utf8::new();
540        let mut count = 0usize;
541        let mut put = |b: u8, charset: Charset, count: &mut usize| -> Result<()> {
542            let decoded = [0xc0 | (b >> 6), 0x80 | (b & 0x3f)];
543            let bytes: &[u8] = if charset == Charset::Iso8859_1 && b >= 0x80 {
544                &decoded
545            } else {
546                core::slice::from_ref(&b)
547            };
548            for &x in bytes {
549                if !utf8.push(x) {
550                    return err(ErrorKind::InvalidUtf8, 0);
551                }
552                emit(x);
553                *count += 1;
554            }
555            Ok(())
556        };
557        for (k, slot) in used.iter().enumerate() {
558            let Some((ext, w)) = *slot else { continue };
559            let raw: &[u8] = match (k, ext, w) {
560                (0, true, Word::Token(t)) => {
561                    let e = ExtValue::parse(t)?;
562                    charset = e.charset();
563                    e.encoded()
564                }
565                (_, true, Word::Token(t)) => t,
566                _ => &[],
567            };
568            if ext {
569                let mut i = 0usize;
570                while let Some(&b) = raw.get(i) {
571                    let byte = if b == b'%' {
572                        let hi = raw
573                            .get(i + 1)
574                            .copied()
575                            .and_then(class::hex_value)
576                            .unwrap_or(0);
577                        let lo = raw
578                            .get(i + 2)
579                            .copied()
580                            .and_then(class::hex_value)
581                            .unwrap_or(0);
582                        i += 3;
583                        (hi << 4) | lo
584                    } else {
585                        i += 1;
586                        b
587                    };
588                    if byte < 0x20 || byte == 0x7f {
589                        return err(ErrorKind::ControlInDecoded, 0);
590                    }
591                    if charset == Charset::UsAscii && byte >= 0x80 {
592                        return err(ErrorKind::InvalidByte, 0);
593                    }
594                    put(byte, charset, &mut count)?;
595                }
596            } else {
597                match w {
598                    Word::Token(t) => {
599                        for &byte in t {
600                            put(byte, charset, &mut count)?;
601                        }
602                    }
603                    Word::Quoted(q) => {
604                        for byte in q.bytes() {
605                            put(byte, charset, &mut count)?;
606                        }
607                    }
608                }
609            }
610        }
611        if !utf8.is_complete() {
612            return err(ErrorKind::InvalidUtf8, 0);
613        }
614        Ok(count)
615    }
616}
617
618/// The bytes a parameter name may hold: MIME token characters except `*`,
619/// `'`, and `%`, which RFC 2231 gives meaning to.
620fn is_attribute(name: &[u8]) -> bool {
621    !name.is_empty()
622        && name
623            .iter()
624            .all(|&b| class::is_mime_token_char(b) && !matches!(b, b'*' | b'\'' | b'%'))
625}
626
627/// The line length writers keep to.
628const LINE: usize = 78;
629
630/// The length of `b` in an extended value: 1 for an attr-char, else 3.
631const fn unit_len(b: u8) -> usize {
632    if class::is_attr_char(b) { 1 } else { 3 }
633}
634
635/// Writes `b` in an extended value: itself or `%XX`.
636fn put_unit(b: u8, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
637    const HEX: &[u8; 16] = b"0123456789ABCDEF";
638    if class::is_attr_char(b) {
639        out.put(&[b])?;
640    } else {
641        let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
642        let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
643        out.put(&[b'%', hi, lo])?;
644    }
645    Ok(())
646}
647
648fn put_param(name: &str, value: &str, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
649    let (n, v) = (name.as_bytes(), value.as_bytes());
650    if !v.is_empty()
651        && v.iter().all(|&b| class::is_mime_token_char(b))
652        && 3 + n.len() + v.len() <= LINE
653    {
654        out.put(b"; ")?;
655        out.put(n)?;
656        out.put(b"=")?;
657        out.put(v)?;
658        return Ok(());
659    }
660    let escaped = v.iter().filter(|&&b| b == b'"' || b == b'\\').count();
661    if v.iter().all(|&b| (0x20..=0x7e).contains(&b)) && 5 + n.len() + v.len() + escaped <= LINE {
662        out.put(b"; ")?;
663        out.put(n)?;
664        out.put(b"=\"")?;
665        for &b in v {
666            if b == b'"' || b == b'\\' {
667                out.put(b"\\")?;
668            }
669            out.put(&[b])?;
670        }
671        out.put(b"\"")?;
672        return Ok(());
673    }
674    let encoded: usize = v.iter().map(|&b| unit_len(b)).sum();
675    if 4 + n.len() + 7 + encoded <= LINE {
676        out.put(b"; ")?;
677        out.put(n)?;
678        out.put(b"*=UTF-8''")?;
679        for &b in v {
680            put_unit(b, out)?;
681        }
682        return Ok(());
683    }
684    // Sections, one per line: `;CRLF name*k*=...`, each line within 78
685    // columns with its trailing `;`.
686    out.put(b";")?;
687    let mut rest = v;
688    let mut k = 0usize;
689    while !rest.is_empty() || k == 0 {
690        if k >= usize::from(MAX_RFC2231_SECTIONS) {
691            return Err(WriteError::OutOfRange);
692        }
693        let mut digits = [0u8; 2];
694        let dl = if k < 10 {
695            digits[0] = b'0' + k as u8;
696            1
697        } else {
698            digits[0] = b'0' + (k / 10) as u8;
699            digits[1] = b'0' + (k % 10) as u8;
700            2
701        };
702        if k > 0 {
703            out.put(b";")?;
704        }
705        out.put(b"\r\n ")?;
706        out.put(n)?;
707        out.put(b"*")?;
708        out.put(digits.get(..dl).unwrap_or(&[]))?;
709        out.put(b"*=")?;
710        let mut col = 1 + n.len() + 1 + dl + 2;
711        if k == 0 {
712            out.put(b"UTF-8''")?;
713            col += 7;
714        }
715        let mut took = 0usize;
716        for &b in rest {
717            let u = unit_len(b);
718            // One column stays for the `;` that may follow.
719            if col + u + 1 > LINE && took > 0 {
720                break;
721            }
722            put_unit(b, out)?;
723            col += u;
724            took += 1;
725        }
726        rest = rest.get(took..).unwrap_or(&[]);
727        k += 1;
728    }
729    Ok(())
730}
731
732/// Writes `; name=value`: as a token, as a quoted string, or, for non-ASCII
733/// or long values, as `; name*=UTF-8''...`, split into `name*0*`,
734/// `name*1*` sections on folded lines when one line would pass 78 columns.
735/// The output parses back with [`parse_mime_params`] to the same value.
736///
737/// # Errors
738///
739/// [`WriteError::NotToken`] for a name that is not a MIME token or holds `*`,
740/// `'`, or `%`; [`WriteError::Forbidden`] for a control character in
741/// `value`; [`WriteError::OutOfRange`] for a value needing more than 32
742/// sections; [`WriteError::Full`] (nothing written when the sink reports its
743/// room).
744///
745/// # Examples
746///
747/// ```
748/// use parse_net::{mail, SliceSink};
749///
750/// let mut buf = [0u8; 64];
751/// let mut out = SliceSink::new(&mut buf);
752/// mail::write_mime_param("filename", "caf\u{e9}.txt", &mut out)?;
753/// assert_eq!(out.written(), b"; filename*=UTF-8''caf%C3%A9.txt");
754/// # Ok::<(), parse_net::WriteError>(())
755/// ```
756pub fn write_mime_param(
757    name: &str,
758    value: &str,
759    out: &mut impl Sink,
760) -> core::result::Result<(), WriteError> {
761    if !is_attribute(name.as_bytes()) {
762        return Err(WriteError::NotToken);
763    }
764    if let Some((at, _)) = value.char_indices().find(|&(_, c)| c.is_control()) {
765        return Err(WriteError::forbidden(at));
766    }
767    let mut count = CountSink::new();
768    put_param(name, value, &mut count)?;
769    reserve(out, count.count())?;
770    put_param(name, value, out)
771}
772
773#[cfg(test)]
774mod tests {
775    use super::*;
776    use std::{format, string::String, vec, vec::Vec};
777
778    fn p(v: &[u8]) -> MimeParams<'_> {
779        parse_mime_params(v, &Limits::DEFAULT).unwrap().0
780    }
781
782    fn k(v: &[u8]) -> ErrorKind {
783        parse_mime_params(v, &Limits::DEFAULT).unwrap_err().kind()
784    }
785
786    fn get(v: &[u8], name: &str) -> String {
787        let mut buf = vec![0u8; 512];
788        String::from(p(v).decode_into(name, &mut buf).unwrap().unwrap())
789    }
790
791    #[test]
792    fn test_mime_params_basic() {
793        assert_eq!(get(b"; a=b; c=\"d e\"", "C"), "d e");
794        assert_eq!(get(b"; boundary={x}", "boundary"), "{x}");
795        assert_eq!(get(b" ; a = (c) b (d)", "a"), "b");
796        assert_eq!(get(b";\r\n a=b", "a"), "b");
797        assert!(
798            p(b"; a=b")
799                .decode_into("z", &mut [0u8; 4])
800                .unwrap()
801                .is_none()
802        );
803        assert_eq!(parse_mime_params(b"", &Limits::DEFAULT).unwrap().1, 0);
804        assert_eq!(
805            parse_mime_params(b"; a=b x", &Limits::DEFAULT).unwrap().1,
806            5
807        );
808        assert_eq!(k(b"; =b"), ErrorKind::NotToken);
809        assert_eq!(k(b"; a"), ErrorKind::MissingValue);
810        assert_eq!(k(b"; a="), ErrorKind::MissingValue);
811        assert_eq!(k(b"; a=\"b"), ErrorKind::UnterminatedQuote);
812        assert_eq!(k(b"; a=1; A=2"), ErrorKind::DuplicateParameter);
813        assert_eq!(k(b"; a=1 (x"), ErrorKind::UnbalancedComment);
814        let l = Limits::DEFAULT.max_params(2);
815        assert!(parse_mime_params(b"; a=1; b=2", &l).is_ok());
816        assert_eq!(
817            parse_mime_params(b"; a=1; b=2; c=3", &l)
818                .unwrap_err()
819                .kind(),
820            ErrorKind::TooManyMembers
821        );
822    }
823
824    #[test]
825    fn test_mime_params_rfc2231() {
826        assert_eq!(
827            get(
828                b"; t*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
829                "t"
830            ),
831            "This is ***fun***"
832        );
833        assert_eq!(get(b"; t*0=\"foo\"; t*1=bar", "t"), "foobar");
834        assert_eq!(get(b"; t*1=bar; t*0=\"foo\"", "t"), "foobar");
835        assert_eq!(get(b"; t*0*=UTF-8''%E2%82; t*1*=%AC", "t"), "\u{20ac}");
836        assert_eq!(get(b"; t=plain; t*=UTF-8''ext", "t"), "ext");
837        assert_eq!(get(b"; t=plain; t*0=cont", "t"), "cont");
838        assert_eq!(get(b"; t*00=x; t=plain", "t"), "plain");
839        assert_eq!(k(b"; t*0=a; t*2=c"), ErrorKind::SectionGap);
840        assert_eq!(k(b"; t*1=a"), ErrorKind::SectionGap);
841        assert_eq!(k(b"; t*0=a; t*0=b"), ErrorKind::DuplicateParameter);
842        assert_eq!(
843            k(b"; t*0*=UTF-8''a; t*1*=UTF-8''b"),
844            ErrorKind::InvalidPercentEncoding
845        );
846        assert_eq!(k(b"; t*0*=UTF-8''%C3"), ErrorKind::InvalidUtf8);
847        assert_eq!(k(b"; t*0*=UTF-8''%0D"), ErrorKind::ControlInDecoded);
848        assert_eq!(k(b"; t*=\"UTF-8''x\""), ErrorKind::InvalidCharset);
849        let mut v = Vec::new();
850        for i in 0..32 {
851            v.extend_from_slice(format!("; t*{i}=x").as_bytes());
852        }
853        assert_eq!(
854            p(&v)
855                .get("t")
856                .map(|m| matches!(m, MimeValue::Continued(c) if c.sections() == 32)),
857            Some(true)
858        );
859        v.extend_from_slice(b"; t*32=x");
860        assert_eq!(k(&v), ErrorKind::TooManySections);
861        let l = Limits::DEFAULT.max_rfc2231_sections(2);
862        assert!(parse_mime_params(b"; t*0=a; t*1=b", &l).is_ok());
863        assert_eq!(
864            parse_mime_params(b"; t*0=a; t*1=b; t*2=c", &l)
865                .unwrap_err()
866                .kind(),
867            ErrorKind::TooManySections
868        );
869    }
870
871    fn written(name: &str, value: &str) -> Vec<u8> {
872        let mut buf = vec![0u8; 4096];
873        let mut out = crate::SliceSink::new(&mut buf);
874        write_mime_param(name, value, &mut out).unwrap();
875        out.written().to_vec()
876    }
877
878    #[test]
879    fn test_write_mime_param_forms_and_round_trip() {
880        assert_eq!(written("charset", "utf-8"), b"; charset=utf-8");
881        assert_eq!(written("name", "a \"b\""), b"; name=\"a \\\"b\\\"\"");
882        assert_eq!(written("name", ""), b"; name=\"\"");
883        let long = "x".repeat(200);
884        let nonascii = "\u{e9}".repeat(60);
885        for value in [
886            String::from("plain"),
887            long,
888            nonascii,
889            String::from("caf\u{e9} au lait.txt"),
890        ] {
891            let w = written("filename", &value);
892            for line in w.split(|&b| b == b'\n') {
893                assert!(line.len() <= 79, "{}", String::from_utf8_lossy(line));
894            }
895            let (params, used) = parse_mime_params(&w, &Limits::DEFAULT).unwrap();
896            assert_eq!(used, w.len());
897            let mut buf = vec![0u8; 1024];
898            assert_eq!(
899                params.decode_into("filename", &mut buf).unwrap(),
900                Some(value.as_str())
901            );
902        }
903        let mut buf = [0u8; 8];
904        let mut out = crate::SliceSink::new(&mut buf);
905        assert_eq!(
906            write_mime_param("a*b", "x", &mut out),
907            Err(WriteError::NotToken)
908        );
909        assert_eq!(
910            write_mime_param("a", "x\ny", &mut out),
911            Err(WriteError::Forbidden { at: 1 })
912        );
913        assert_eq!(
914            write_mime_param("a", "toolongvalue", &mut out),
915            Err(WriteError::Full)
916        );
917        assert!(out.is_empty());
918        let mut big = vec![0u8; 1 << 16];
919        let mut out = crate::SliceSink::new(&mut big);
920        assert_eq!(
921            write_mime_param("f", &"\u{e9}".repeat(1000), &mut out),
922            Err(WriteError::OutOfRange)
923        );
924    }
925}