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