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, is_mime_attribute_char},
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.iter().all(u8::is_ascii_digit)
42            && (digits.len() == 1 || digits.first() != Some(&b'0'));
43        if canonical {
44            // Saturating: any number this large is past the section limit.
45            let n = digits.iter().fold(0usize, |a, &d| {
46                a.saturating_mul(10).saturating_add(usize::from(d - b'0'))
47            });
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 is_mime_attribute_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_mime(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                let at = (name.as_ptr() as usize).saturating_sub(s.as_ptr() as usize);
204                return err(ErrorKind::SectionGap, at);
205            }
206        }
207    }
208    Ok((params, i))
209}
210
211/// Validated MIME parameters.
212///
213/// # Examples
214///
215/// ```
216/// use parse_net::{mail, Limits};
217///
218/// let (p, _) = mail::parse_mime_params(b"; charset=us-ascii", &Limits::DEFAULT)?;
219/// let mut buf = [0u8; 16];
220/// assert_eq!(p.decode_into("charset", &mut buf)?, Some("us-ascii"));
221/// # Ok::<(), parse_net::ParseError>(())
222/// ```
223#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
224pub struct MimeParams<'a> {
225    text: &'a [u8],
226    count: usize,
227}
228
229impl<'a> MimeParams<'a> {
230    /// The value of `name` (case-insensitive): a continuation when
231    /// `name*0` or `name*0*` is present, else the extended `name*`, else the
232    /// plain `name`.
233    ///
234    /// # Examples
235    ///
236    /// ```
237    /// use parse_net::{mail::{self, MimeValue}, Limits};
238    ///
239    /// let (p, _) = mail::parse_mime_params(b"; name=plain.txt; name*=UTF-8''ext.txt", &Limits::DEFAULT)?;
240    /// assert!(matches!(p.get("NAME"), Some(MimeValue::Extended(e)) if e.encoded() == b"ext.txt"));
241    /// assert!(p.get("other").is_none());
242    /// # Ok::<(), parse_net::ParseError>(())
243    /// ```
244    #[must_use]
245    pub fn get(&self, name: &str) -> Option<MimeValue<'a>> {
246        let want = name.as_bytes();
247        let mut ext = None;
248        let mut plain = None;
249        for (n, w) in self.iter() {
250            match split_name(n) {
251                (b, Section::Part { n: 0, .. }) if b.eq_ignore_ascii_case(want) => {
252                    return Some(MimeValue::Continued(Continuation {
253                        params: *self,
254                        name: b,
255                    }));
256                }
257                (b, Section::Ext) if b.eq_ignore_ascii_case(want) => {
258                    if let Word::Token(t) = w {
259                        ext = ExtValue::parse_mime(t).ok();
260                    }
261                }
262                (b, Section::Plain) if b.eq_ignore_ascii_case(want) => plain = Some(w),
263                _ => {}
264            }
265        }
266        ext.map(MimeValue::Extended).or(plain.map(MimeValue::Plain))
267    }
268
269    /// Decodes the value of `name` into `out` (as [`get`](Self::get)
270    /// chooses it); `Ok(None)` when absent.
271    ///
272    /// # Errors
273    ///
274    /// [`ErrorKind::BufferTooSmall`]; [`ErrorKind::InvalidUtf8`] for a plain
275    /// value that is not UTF-8. Nothing is written on error.
276    ///
277    /// # Examples
278    ///
279    /// ```
280    /// use parse_net::{mail, ErrorKind, Limits};
281    ///
282    /// let (p, _) = mail::parse_mime_params(b"; boundary=\"simple boundary\"", &Limits::DEFAULT)?;
283    /// let mut buf = [0u8; 32];
284    /// assert_eq!(p.decode_into("boundary", &mut buf)?, Some("simple boundary"));
285    /// assert_eq!(p.decode_into("charset", &mut buf)?, None);
286    /// assert_eq!(p.decode_into("boundary", &mut [0u8; 4]).unwrap_err().kind(), ErrorKind::BufferTooSmall);
287    /// # Ok::<(), parse_net::ParseError>(())
288    /// ```
289    pub fn decode_into<'b>(&self, name: &str, out: &'b mut [u8]) -> Result<Option<&'b str>> {
290        match self.get(name) {
291            None => Ok(None),
292            Some(MimeValue::Continued(c)) => c.decode_into(out).map(Some),
293            Some(MimeValue::Extended(e)) => e.decode_into(out).map(Some),
294            Some(MimeValue::Plain(w)) => {
295                let Some(dst) = out.get_mut(..w.len()) else {
296                    return err(ErrorKind::BufferTooSmall, 0);
297                };
298                // Checked before anything is written.
299                let (mut utf8, text) = (crate::utf8::Utf8::new(), self.value_text(&w));
300                let valid = match w {
301                    Word::Token(t) => t.iter().all(|&b| utf8.push(b)),
302                    Word::Quoted(q) => q.bytes().all(|b| utf8.push(b)),
303                };
304                if !valid || !utf8.is_complete() {
305                    return err(ErrorKind::InvalidUtf8, text);
306                }
307                core::str::from_utf8(w.unescape_into(dst)?)
308                    .map(Some)
309                    .map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
310            }
311        }
312    }
313
314    /// The offset of a value in the text.
315    fn value_text(&self, w: &Word<'_>) -> usize {
316        let raw = match w {
317            Word::Token(t) => t,
318            Word::Quoted(q) => q.raw(),
319        };
320        (raw.as_ptr() as usize).saturating_sub(self.text.as_ptr() as usize)
321    }
322
323    /// The parameters as sent, sections included, in order.
324    ///
325    /// # Examples
326    ///
327    /// ```
328    /// use parse_net::{mail, Limits};
329    ///
330    /// let (p, _) = mail::parse_mime_params(b"; a*0=x; a*1=y", &Limits::DEFAULT)?;
331    /// let names: Vec<&[u8]> = p.iter().map(|(name, _)| name).collect();
332    /// assert_eq!(names, [&b"a*0"[..], b"a*1"]);
333    /// # Ok::<(), parse_net::ParseError>(())
334    /// ```
335    #[must_use]
336    pub fn iter(&self) -> MimeParamIter<'a> {
337        MimeParamIter { s: self.text, i: 0 }
338    }
339
340    /// The number of parameters, sections counted one by one.
341    ///
342    /// # Examples
343    ///
344    /// ```
345    /// use parse_net::{mail, Limits};
346    ///
347    /// let (p, _) = mail::parse_mime_params(b"; a*0=x; a*1=y; b=z", &Limits::DEFAULT)?;
348    /// assert_eq!(p.len(), 3);
349    /// # Ok::<(), parse_net::ParseError>(())
350    /// ```
351    #[must_use]
352    pub const fn len(&self) -> usize {
353        self.count
354    }
355
356    /// Whether there are no parameters.
357    ///
358    /// # Examples
359    ///
360    /// ```
361    /// use parse_net::{mail, Limits};
362    ///
363    /// let (p, used) = mail::parse_mime_params(b" no params", &Limits::DEFAULT)?;
364    /// assert!(p.is_empty() && used == 0);
365    /// # Ok::<(), parse_net::ParseError>(())
366    /// ```
367    #[must_use]
368    pub const fn is_empty(&self) -> bool {
369        self.count == 0
370    }
371}
372
373/// The parameters of a validated [`MimeParams`].
374///
375/// # Examples
376///
377/// ```
378/// use parse_net::{mail, Limits};
379///
380/// let (p, _) = mail::parse_mime_params(b"; format=flowed", &Limits::DEFAULT)?;
381/// let mut it = p.iter();
382/// let (name, value) = it.next().expect("one parameter");
383/// assert!(name == b"format" && value.eq_bytes(b"flowed"));
384/// assert!(it.next().is_none());
385/// # Ok::<(), parse_net::ParseError>(())
386/// ```
387#[derive(Clone, Debug)]
388pub struct MimeParamIter<'a> {
389    s: &'a [u8],
390    i: usize,
391}
392
393impl<'a> Iterator for MimeParamIter<'a> {
394    type Item = (&'a [u8], Word<'a>);
395
396    fn next(&mut self) -> Option<Self::Item> {
397        let j = cfws(self.s, self.i, &RELAXED).ok()?;
398        if self.s.get(j) != Some(&b';') {
399            return None;
400        }
401        let (name, value, _, end) = param_at(self.s, j, &RELAXED).ok()?;
402        self.i = end;
403        Some((name, value))
404    }
405}
406
407/// A MIME parameter value.
408///
409/// # Examples
410///
411/// ```
412/// use parse_net::{mail::{self, MimeValue}, Limits};
413///
414/// let (p, _) = mail::parse_mime_params(b"; size=10; t*0=a; t*1=b", &Limits::DEFAULT)?;
415/// assert!(matches!(p.get("size"), Some(MimeValue::Plain(w)) if w.eq_bytes(b"10")));
416/// assert!(matches!(p.get("t"), Some(MimeValue::Continued(_))));
417/// # Ok::<(), parse_net::ParseError>(())
418/// ```
419#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
420pub enum MimeValue<'a> {
421    /// `name=value`.
422    Plain(Word<'a>),
423    /// `name*=charset'lang'value`.
424    Extended(ExtValue<'a>),
425    /// `name*0`, `name*1*`, ...: RFC 2231 continuations.
426    Continued(Continuation<'a>),
427}
428
429/// The sections of an RFC 2231 continued value, validated at parse.
430///
431/// # Examples
432///
433/// ```
434/// use parse_net::{mail::{self, MimeValue}, Limits};
435///
436/// let (p, _) = mail::parse_mime_params(b"; url*0=\"http://a.example/\"; url*1=\"b\"", &Limits::DEFAULT)?;
437/// let Some(MimeValue::Continued(c)) = p.get("url") else { unreachable!() };
438/// let mut buf = [0u8; 32];
439/// assert_eq!(c.decode_into(&mut buf)?, "http://a.example/b");
440/// # Ok::<(), parse_net::ParseError>(())
441/// ```
442#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
443pub struct Continuation<'a> {
444    params: MimeParams<'a>,
445    name: &'a [u8],
446}
447
448impl Continuation<'_> {
449    /// The number of sections.
450    ///
451    /// # Examples
452    ///
453    /// ```
454    /// use parse_net::{mail::{self, MimeValue}, Limits};
455    ///
456    /// let (p, _) = mail::parse_mime_params(b"; n*0=a; n*1=b; n*2=c", &Limits::DEFAULT)?;
457    /// let Some(MimeValue::Continued(c)) = p.get("n") else { unreachable!() };
458    /// assert_eq!(c.sections(), 3);
459    /// # Ok::<(), parse_net::ParseError>(())
460    /// ```
461    #[must_use]
462    pub fn sections(&self) -> usize {
463        self.params
464            .iter()
465            .filter(|(n, _)| matches!(split_name(n), (b, Section::Part { .. }) if b.eq_ignore_ascii_case(self.name)))
466            .count()
467    }
468
469    /// An upper bound on the decoded length (twice the raw length, for
470    /// ISO-8859-1 transcoded to UTF-8).
471    ///
472    /// # Examples
473    ///
474    /// ```
475    /// use parse_net::{mail::{self, MimeValue}, Limits};
476    ///
477    /// let (p, _) = mail::parse_mime_params(b"; t*0=abc; t*1=de", &Limits::DEFAULT)?;
478    /// let Some(MimeValue::Continued(c)) = p.get("t") else { unreachable!() };
479    /// assert_eq!(c.max_decoded_len(), 10);
480    /// let mut buf = [0u8; 10];
481    /// assert_eq!(c.decode_into(&mut buf)?, "abcde");
482    /// # Ok::<(), parse_net::ParseError>(())
483    /// ```
484    #[must_use]
485    pub fn max_decoded_len(&self) -> usize {
486        self.params
487            .iter()
488            .filter(|(n, _)| matches!(split_name(n), (b, Section::Part { .. }) if b.eq_ignore_ascii_case(self.name)))
489            .map(|(_, w)| w.len().saturating_mul(2))
490            .sum()
491    }
492
493    /// Decodes the sections in order into `out`: `%XX` in extended sections,
494    /// the charset of section 0 (UTF-8 or ISO-8859-1, transcoded).
495    ///
496    /// # Errors
497    ///
498    /// [`ErrorKind::BufferTooSmall`]; nothing is written on error.
499    ///
500    /// # Examples
501    ///
502    /// ```
503    /// use parse_net::{mail::{self, MimeValue}, Limits};
504    ///
505    /// let (p, _) = mail::parse_mime_params(b"; title*0*=iso-8859-1'en'%A3; title*1=\" rates\"", &Limits::DEFAULT)?;
506    /// let Some(MimeValue::Continued(c)) = p.get("title") else { unreachable!() };
507    /// let mut buf = [0u8; 16];
508    /// assert_eq!((c.sections(), c.decode_into(&mut buf)?), (2, "\u{a3} rates"));
509    /// # Ok::<(), parse_net::ParseError>(())
510    /// ```
511    pub fn decode_into<'b>(&self, out: &'b mut [u8]) -> Result<&'b str> {
512        let len = self.walk(&RELAXED, &mut |_| {})?;
513        let Some(dst) = out.get_mut(..len) else {
514            return err(ErrorKind::BufferTooSmall, 0);
515        };
516        let mut n = 0usize;
517        let _ = self.walk(&RELAXED, &mut |b: u8| {
518            if let Some(d) = dst.get_mut(n) {
519                *d = b;
520            }
521            n += 1;
522        })?;
523        core::str::from_utf8(dst)
524            .map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
525    }
526
527    /// Orders the sections, checks them, and emits the decoded UTF-8 bytes;
528    /// returns their count. A section given twice is refused, so the value
529    /// cannot depend on the order of the parameters.
530    fn walk(&self, limits: &Limits, emit: &mut impl FnMut(u8)) -> Result<usize> {
531        let base = self.params.text.as_ptr() as usize;
532        let at = |s: &[u8]| (s.as_ptr() as usize).saturating_sub(base);
533        let mut slots: [Option<(bool, Word<'_>)>; MAX_RFC2231_SECTIONS as usize] =
534            [None; MAX_RFC2231_SECTIONS as usize];
535        let max = usize::from(limits.rfc2231_sections.min(MAX_RFC2231_SECTIONS));
536        let mut highest = 0usize;
537        for (name, w) in self.params.iter() {
538            if let (b, Section::Part { n, ext }) = split_name(name) {
539                if !b.eq_ignore_ascii_case(self.name) {
540                    continue;
541                }
542                if n >= max {
543                    return err(ErrorKind::TooManySections, at(name));
544                }
545                match slots.get_mut(n) {
546                    Some(Some(_)) => return err(ErrorKind::SectionGap, at(name)),
547                    Some(slot) => *slot = Some((ext, w)),
548                    None => {}
549                }
550                highest = highest.max(n);
551            }
552        }
553        let used = slots.get(..=highest).unwrap_or(&[]);
554        if let Some(gap) = used.iter().position(Option::is_none) {
555            // The section after the gap is the first that cannot be accepted.
556            let after = self.params.iter().find(|(name, _)| {
557                matches!(split_name(name), (b, Section::Part { n, .. })
558                    if b.eq_ignore_ascii_case(self.name) && n > gap)
559            });
560            let offset = after.map_or(0, |(name, _)| at(name));
561            return err(ErrorKind::SectionGap, offset);
562        }
563        let mut charset = Charset::Utf8;
564        let mut utf8 = crate::utf8::Utf8::new();
565        let (mut count, mut prev) = (0usize, 0u8);
566        // Every section passes through here, so what spans two sections is
567        // seen whole: `strict` also refuses HTAB, for decoded bytes.
568        let mut put = |b: u8, charset: Charset, strict: bool, off: usize| -> Result<()> {
569            if (b < 0x20 && (strict || b != b'\t')) || b == 0x7f {
570                return err(ErrorKind::ControlInDecoded, off);
571            }
572            if charset == Charset::UsAscii && b >= 0x80 {
573                return err(ErrorKind::InvalidByte, off);
574            }
575            let c1 = if charset == Charset::Iso8859_1 {
576                (0x80..=0x9f).contains(&b)
577            } else {
578                prev == 0xc2 && (0x80..=0x9f).contains(&b)
579            };
580            if c1 {
581                return err(ErrorKind::ControlInDecoded, off);
582            }
583            prev = b;
584            let decoded = [0xc0 | (b >> 6), 0x80 | (b & 0x3f)];
585            let bytes: &[u8] = if charset == Charset::Iso8859_1 && b >= 0x80 {
586                &decoded
587            } else {
588                core::slice::from_ref(&b)
589            };
590            for &x in bytes {
591                if !utf8.push(x) {
592                    return err(ErrorKind::InvalidUtf8, off);
593                }
594                emit(x);
595                count += 1;
596            }
597            Ok(())
598        };
599        for (k, slot) in used.iter().enumerate() {
600            let Some((ext, w)) = *slot else { continue };
601            let raw: &[u8] = match (k, ext, w) {
602                (0, true, Word::Token(t)) => {
603                    let e = ExtValue::parse_mime(t)?;
604                    charset = e.charset();
605                    e.encoded()
606                }
607                (_, true, Word::Token(t)) => t,
608                _ => &[],
609            };
610            if ext {
611                let mut i = 0usize;
612                while let Some(&b) = raw.get(i) {
613                    let start = i;
614                    let byte = if b == b'%' {
615                        let hi = raw
616                            .get(i + 1)
617                            .copied()
618                            .and_then(class::hex_value)
619                            .unwrap_or(0);
620                        let lo = raw
621                            .get(i + 2)
622                            .copied()
623                            .and_then(class::hex_value)
624                            .unwrap_or(0);
625                        i += 3;
626                        (hi << 4) | lo
627                    } else {
628                        i += 1;
629                        b
630                    };
631                    put(byte, charset, true, at(raw) + start)?;
632                }
633            } else {
634                match w {
635                    Word::Token(t) => {
636                        for (i, &byte) in t.iter().enumerate() {
637                            put(byte, charset, false, at(t) + i)?;
638                        }
639                    }
640                    Word::Quoted(q) => {
641                        for byte in q.bytes() {
642                            put(byte, charset, false, at(q.raw()))?;
643                        }
644                    }
645                }
646            }
647        }
648        if !utf8.is_complete() {
649            return err(ErrorKind::InvalidUtf8, self.params.text.len());
650        }
651        Ok(count)
652    }
653}
654
655/// The bytes a parameter name may hold: MIME token characters except `*`,
656/// `'`, and `%`, which RFC 2231 gives meaning to.
657fn is_attribute(name: &[u8]) -> bool {
658    !name.is_empty()
659        && name
660            .iter()
661            .all(|&b| class::is_mime_token_char(b) && !matches!(b, b'*' | b'\'' | b'%'))
662}
663
664/// The line length writers keep to.
665const LINE: usize = 78;
666
667/// The length of `b` in an extended value: 1 for an attr-char, else 3.
668const fn unit_len(b: u8) -> usize {
669    if class::is_attr_char(b) { 1 } else { 3 }
670}
671
672/// Writes `b` in an extended value: itself or `%XX`.
673fn put_unit(b: u8, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
674    const HEX: &[u8; 16] = b"0123456789ABCDEF";
675    if class::is_attr_char(b) {
676        out.put(&[b])?;
677    } else {
678        let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
679        let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
680        out.put(&[b'%', hi, lo])?;
681    }
682    Ok(())
683}
684
685/// The one-line forms of a parameter value.
686#[derive(Clone, Copy)]
687enum Form {
688    Token,
689    Quoted,
690    Extended,
691}
692
693/// The one-line form `value` takes, and the length of `; name=value` in it.
694fn form(n: &[u8], v: &[u8]) -> (Form, usize) {
695    // Each test stops at the first byte that fails it; the printable test
696    // and the count of escapes are simple enough to vectorize.
697    if !v.is_empty() && v.iter().all(|&b| class::is_mime_token_char(b)) {
698        return (Form::Token, 3 + n.len() + v.len());
699    }
700    if v.iter().all(|&b| (0x20..=0x7e).contains(&b)) {
701        let escaped = v.iter().filter(|&&b| b == b'"' || b == b'\\').count();
702        return (Form::Quoted, 5 + n.len() + v.len() + escaped);
703    }
704    let encoded: usize = v.iter().map(|&b| unit_len(b)).sum();
705    (Form::Extended, 11 + n.len() + encoded)
706}
707
708/// Writes `name=value` in `form`, without the `; ` before it.
709fn put_form(
710    form: Form,
711    n: &[u8],
712    v: &[u8],
713    out: &mut impl Sink,
714) -> core::result::Result<(), WriteError> {
715    out.put(n)?;
716    match form {
717        Form::Token => {
718            out.put(b"=")?;
719            out.put(v)?;
720        }
721        Form::Quoted => {
722            out.put(b"=\"")?;
723            // The runs between the bytes that need a `\` go out whole.
724            let mut rest = v;
725            while let Some(p) = rest.iter().position(|&b| b == b'"' || b == b'\\') {
726                out.put(rest.get(..p).unwrap_or(&[]))?;
727                out.put(b"\\")?;
728                out.put(rest.get(p..p + 1).unwrap_or(&[]))?;
729                rest = rest.get(p + 1..).unwrap_or(&[]);
730            }
731            out.put(rest)?;
732            out.put(b"\"")?;
733        }
734        Form::Extended => {
735            out.put(b"*=UTF-8''")?;
736            let mut rest = v;
737            while let Some(p) = rest.iter().position(|&b| !class::is_attr_char(b)) {
738                out.put(rest.get(..p).unwrap_or(&[]))?;
739                put_unit(rest.get(p).copied().unwrap_or(0), out)?;
740                rest = rest.get(p + 1..).unwrap_or(&[]);
741            }
742            out.put(rest)?;
743        }
744    }
745    Ok(())
746}
747
748/// Writes the parameter, whose one-line form is `f` of `len` bytes, where
749/// `room` columns are left on the line, and returns the columns left on the
750/// line it ends on.
751fn put_param(
752    n: &[u8],
753    v: &[u8],
754    (f, len): (Form, usize),
755    room: usize,
756    out: &mut impl Sink,
757) -> core::result::Result<usize, WriteError> {
758    if len <= room {
759        out.put(b"; ")?;
760        put_form(f, n, v, out)?;
761        return Ok(room - len);
762    }
763    // On a line of its own, ` name=value` keeps one column for the `;` that
764    // may follow, as the sections do.
765    if len <= LINE {
766        out.put(b";\r\n ")?;
767        put_form(f, n, v, out)?;
768        return Ok(LINE - (len - 1));
769    }
770    // Sections, one per line: `;CRLF name*k*=...`, each line within 78
771    // columns with its trailing `;`. A name that leaves no room for one
772    // encoded byte after `*kk*=UTF-8''` cannot be written.
773    if 1 + n.len() + 12 + 3 + 1 > LINE {
774        return Err(WriteError::OutOfRange);
775    }
776    out.put(b";")?;
777    let mut rest = v;
778    let mut k = 0usize;
779    let mut col = 0usize;
780    while !rest.is_empty() || k == 0 {
781        if k >= usize::from(MAX_RFC2231_SECTIONS) {
782            return Err(WriteError::OutOfRange);
783        }
784        let mut digits = [0u8; 2];
785        let dl = if k < 10 {
786            digits[0] = b'0' + k as u8;
787            1
788        } else {
789            digits[0] = b'0' + (k / 10) as u8;
790            digits[1] = b'0' + (k % 10) as u8;
791            2
792        };
793        if k > 0 {
794            out.put(b";")?;
795        }
796        out.put(b"\r\n ")?;
797        out.put(n)?;
798        out.put(b"*")?;
799        out.put(digits.get(..dl).unwrap_or(&[]))?;
800        out.put(b"*=")?;
801        col = 1 + n.len() + 1 + dl + 2;
802        if k == 0 {
803            out.put(b"UTF-8''")?;
804            col += 7;
805        }
806        let mut took = 0usize;
807        for &b in rest {
808            let u = unit_len(b);
809            // One column stays for the `;` that may follow.
810            if col + u + 1 > LINE && took > 0 {
811                break;
812            }
813            put_unit(b, out)?;
814            col += u;
815            took += 1;
816        }
817        rest = rest.get(took..).unwrap_or(&[]);
818        k += 1;
819    }
820    Ok(LINE.saturating_sub(col))
821}
822
823/// Checks the name and value, then writes the parameter with nothing written
824/// when the sink reports too little room.
825fn write_param(
826    name: &str,
827    value: &str,
828    room: usize,
829    out: &mut impl Sink,
830) -> core::result::Result<usize, WriteError> {
831    if !is_attribute(name.as_bytes()) {
832        return Err(WriteError::NotToken);
833    }
834    // ASCII text holds no C1 control, so its bytes are tested alone.
835    let control = if value.is_ascii() {
836        value.bytes().position(|b| b < 0x20 || b == 0x7f)
837    } else {
838        value
839            .char_indices()
840            .find(|&(_, c)| c.is_control())
841            .map(|(at, _)| at)
842    };
843    if let Some(at) = control {
844        return Err(WriteError::forbidden(at));
845    }
846    let (n, v) = (name.as_bytes(), value.as_bytes());
847    let f = form(n, v);
848    let mut count = CountSink::new();
849    let _ = put_param(n, v, f, room, &mut count)?;
850    reserve(out, count.count())?;
851    put_param(n, v, f, room, out)
852}
853
854/// Writes `; name=value`: as a token, as a quoted string, or, for non-ASCII
855/// or long values, as `; name*=UTF-8''...`, split into `name*0*`,
856/// `name*1*` sections on folded lines when one line would pass 78 columns.
857/// The parameter starts a line of its own: use [`write_mime_param_at`]
858/// after other text on the line. The output parses back with
859/// [`parse_mime_params`] to the same value.
860///
861/// # Errors
862///
863/// [`WriteError::NotToken`] for a name that is not a MIME token or holds `*`,
864/// `'`, or `%`; [`WriteError::Forbidden`] for a control character in
865/// `value`; [`WriteError::OutOfRange`] for a value needing more than 32
866/// sections; [`WriteError::Full`] (nothing written when the sink reports its
867/// room).
868///
869/// # Examples
870///
871/// ```
872/// use parse_net::{mail, SliceSink};
873///
874/// let mut buf = [0u8; 64];
875/// let mut out = SliceSink::new(&mut buf);
876/// mail::write_mime_param("filename", "caf\u{e9}.txt", &mut out)?;
877/// assert_eq!(out.written(), b"; filename*=UTF-8''caf%C3%A9.txt");
878/// # Ok::<(), parse_net::WriteError>(())
879/// ```
880pub fn write_mime_param(
881    name: &str,
882    value: &str,
883    out: &mut impl Sink,
884) -> core::result::Result<(), WriteError> {
885    write_param(name, value, LINE, out).map(|_| ())
886}
887
888/// Writes `; name=value` as [`write_mime_param`] does, where
889/// `room_first_line` columns are left on the current line (78 less what the
890/// line holds), and returns the columns left on the line the parameter ends
891/// on, for the next call. A one-line form that does not fit in the room goes
892/// on a new line, `;CRLF name=value`, before the value is split into
893/// sections; the `;` always goes on the current line. With a
894/// `room_first_line` of 78 the output is that of [`write_mime_param`].
895///
896/// # Errors
897///
898/// As [`write_mime_param`].
899///
900/// # Examples
901///
902/// ```
903/// use parse_net::{mail, SliceSink};
904///
905/// let mut buf = [0u8; 128];
906/// let mut out = SliceSink::new(&mut buf);
907/// // After `Content-Disposition: attachment`, 47 columns are left.
908/// let room = mail::write_mime_param_at("filename", "quarterly report for the northern region.pdf", 47, &mut out)?;
909/// assert_eq!(out.written(), &b";\r\n filename=\"quarterly report for the northern region.pdf\""[..]);
910/// assert_eq!(room, 22);
911/// let room = mail::write_mime_param_at("size", "1024", room, &mut out)?;
912/// assert_eq!(room, 22 - 11);
913/// # Ok::<(), parse_net::WriteError>(())
914/// ```
915pub fn write_mime_param_at(
916    name: &str,
917    value: &str,
918    room_first_line: usize,
919    out: &mut impl Sink,
920) -> core::result::Result<usize, WriteError> {
921    write_param(name, value, room_first_line, out)
922}
923
924#[cfg(test)]
925mod tests {
926    use super::*;
927    use std::{format, string::String, vec, vec::Vec};
928
929    fn p(v: &[u8]) -> MimeParams<'_> {
930        parse_mime_params(v, &Limits::DEFAULT).unwrap().0
931    }
932
933    fn k(v: &[u8]) -> ErrorKind {
934        parse_mime_params(v, &Limits::DEFAULT).unwrap_err().kind()
935    }
936
937    fn get(v: &[u8], name: &str) -> String {
938        let mut buf = vec![0u8; 512];
939        String::from(p(v).decode_into(name, &mut buf).unwrap().unwrap())
940    }
941
942    #[test]
943    fn test_mime_params_basic() {
944        assert_eq!(get(b"; a=b; c=\"d e\"", "C"), "d e");
945        assert_eq!(get(b"; boundary={x}", "boundary"), "{x}");
946        assert_eq!(get(b" ; a = (c) b (d)", "a"), "b");
947        assert_eq!(get(b";\r\n a=b", "a"), "b");
948        assert!(
949            p(b"; a=b")
950                .decode_into("z", &mut [0u8; 4])
951                .unwrap()
952                .is_none()
953        );
954        assert_eq!(parse_mime_params(b"", &Limits::DEFAULT).unwrap().1, 0);
955        assert_eq!(
956            parse_mime_params(b"; a=b x", &Limits::DEFAULT).unwrap().1,
957            5
958        );
959        assert_eq!(k(b"; =b"), ErrorKind::NotToken);
960        assert_eq!(k(b"; a"), ErrorKind::MissingValue);
961        assert_eq!(k(b"; a="), ErrorKind::MissingValue);
962        assert_eq!(k(b"; a=\"b"), ErrorKind::UnterminatedQuote);
963        assert_eq!(k(b"; a=1; A=2"), ErrorKind::DuplicateParameter);
964        assert_eq!(k(b"; a=1 (x"), ErrorKind::UnbalancedComment);
965        let l = Limits::DEFAULT.max_params(2);
966        assert!(parse_mime_params(b"; a=1; b=2", &l).is_ok());
967        assert_eq!(
968            parse_mime_params(b"; a=1; b=2; c=3", &l)
969                .unwrap_err()
970                .kind(),
971            ErrorKind::TooManyMembers
972        );
973    }
974
975    #[test]
976    fn test_mime_params_rfc2231() {
977        assert_eq!(
978            get(
979                b"; t*=us-ascii'en-us'This%20is%20%2A%2A%2Afun%2A%2A%2A",
980                "t"
981            ),
982            "This is ***fun***"
983        );
984        assert_eq!(get(b"; t*0=\"foo\"; t*1=bar", "t"), "foobar");
985        assert_eq!(get(b"; t*1=bar; t*0=\"foo\"", "t"), "foobar");
986        assert_eq!(get(b"; t*0*=UTF-8''%E2%82; t*1*=%AC", "t"), "\u{20ac}");
987        assert_eq!(get(b"; t=plain; t*=UTF-8''ext", "t"), "ext");
988        assert_eq!(get(b"; t=plain; t*0=cont", "t"), "cont");
989        assert_eq!(get(b"; t*00=x; t=plain", "t"), "plain");
990        assert_eq!(k(b"; t*0=a; t*2=c"), ErrorKind::SectionGap);
991        assert_eq!(k(b"; t*1=a"), ErrorKind::SectionGap);
992        assert_eq!(k(b"; t*0=a; t*0=b"), ErrorKind::DuplicateParameter);
993        assert_eq!(
994            k(b"; t*0*=UTF-8''a; t*1*=UTF-8''b"),
995            ErrorKind::InvalidPercentEncoding
996        );
997        assert_eq!(k(b"; t*0*=UTF-8''%C3"), ErrorKind::InvalidUtf8);
998        assert_eq!(k(b"; t*0*=UTF-8''%0D"), ErrorKind::ControlInDecoded);
999        assert_eq!(k(b"; t*=\"UTF-8''x\""), ErrorKind::InvalidCharset);
1000        let mut v = Vec::new();
1001        for i in 0..32 {
1002            v.extend_from_slice(format!("; t*{i}=x").as_bytes());
1003        }
1004        assert_eq!(
1005            p(&v)
1006                .get("t")
1007                .map(|m| matches!(m, MimeValue::Continued(c) if c.sections() == 32)),
1008            Some(true)
1009        );
1010        v.extend_from_slice(b"; t*32=x");
1011        assert_eq!(k(&v), ErrorKind::TooManySections);
1012        let l = Limits::DEFAULT.max_rfc2231_sections(2);
1013        assert!(parse_mime_params(b"; t*0=a; t*1=b", &l).is_ok());
1014        assert_eq!(
1015            parse_mime_params(b"; t*0=a; t*1=b; t*2=c", &l)
1016                .unwrap_err()
1017                .kind(),
1018            ErrorKind::TooManySections
1019        );
1020    }
1021
1022    fn written(name: &str, value: &str) -> Vec<u8> {
1023        let mut buf = vec![0u8; 4096];
1024        let mut out = crate::SliceSink::new(&mut buf);
1025        write_mime_param(name, value, &mut out).unwrap();
1026        out.written().to_vec()
1027    }
1028
1029    #[test]
1030    fn test_write_mime_param_forms_and_round_trip() {
1031        assert_eq!(written("charset", "utf-8"), b"; charset=utf-8");
1032        assert_eq!(written("name", "a \"b\""), b"; name=\"a \\\"b\\\"\"");
1033        assert_eq!(written("name", ""), b"; name=\"\"");
1034        let long = "x".repeat(200);
1035        let nonascii = "\u{e9}".repeat(60);
1036        for value in [
1037            String::from("plain"),
1038            long,
1039            nonascii,
1040            String::from("caf\u{e9} au lait.txt"),
1041        ] {
1042            let w = written("filename", &value);
1043            for line in w.split(|&b| b == b'\n') {
1044                assert!(line.len() <= 79, "{}", String::from_utf8_lossy(line));
1045            }
1046            let (params, used) = parse_mime_params(&w, &Limits::DEFAULT).unwrap();
1047            assert_eq!(used, w.len());
1048            let mut buf = vec![0u8; 1024];
1049            assert_eq!(
1050                params.decode_into("filename", &mut buf).unwrap(),
1051                Some(value.as_str())
1052            );
1053        }
1054        let mut buf = [0u8; 8];
1055        let mut out = crate::SliceSink::new(&mut buf);
1056        assert_eq!(
1057            write_mime_param("a*b", "x", &mut out),
1058            Err(WriteError::NotToken)
1059        );
1060        assert_eq!(
1061            write_mime_param("a", "x\ny", &mut out),
1062            Err(WriteError::Forbidden { at: 1 })
1063        );
1064        assert_eq!(
1065            write_mime_param("a", "toolongvalue", &mut out),
1066            Err(WriteError::Full)
1067        );
1068        assert!(out.is_empty());
1069        let mut big = vec![0u8; 1 << 16];
1070        let mut out = crate::SliceSink::new(&mut big);
1071        assert_eq!(
1072            write_mime_param("f", &"\u{e9}".repeat(1000), &mut out),
1073            Err(WriteError::OutOfRange)
1074        );
1075    }
1076
1077    fn written_at(name: &str, value: &str, room: usize) -> (String, usize) {
1078        let mut buf = vec![0u8; 4096];
1079        let mut out = crate::SliceSink::new(&mut buf);
1080        let left = write_mime_param_at(name, value, room, &mut out).unwrap();
1081        (String::from_utf8(out.written().to_vec()).unwrap(), left)
1082    }
1083
1084    #[test]
1085    fn test_write_mime_param_at_room() {
1086        // `; charset=utf-8` is 15 columns: it fits in 15, not in 14.
1087        assert_eq!(
1088            written_at("charset", "utf-8", 15),
1089            (String::from("; charset=utf-8"), 0)
1090        );
1091        assert_eq!(
1092            written_at("charset", "utf-8", 14),
1093            (String::from(";\r\n charset=utf-8"), 64)
1094        );
1095        assert_eq!(
1096            written_at("charset", "utf-8", 0),
1097            (String::from(";\r\n charset=utf-8"), 64)
1098        );
1099        assert_eq!(written_at("n", "a b", 9), (String::from("; n=\"a b\""), 0));
1100        assert_eq!(
1101            written_at("n", "a b", 8),
1102            (String::from(";\r\n n=\"a b\""), 70)
1103        );
1104        assert_eq!(
1105            written_at("f", "caf\u{e9}", 21),
1106            (String::from("; f*=UTF-8''caf%C3%A9"), 0)
1107        );
1108        assert_eq!(
1109            written_at("f", "caf\u{e9}", 20),
1110            (String::from(";\r\n f*=UTF-8''caf%C3%A9"), 58)
1111        );
1112        // A one-line form of 78 columns goes on a line of its own, where its
1113        // 77 leave the column for a `;`.
1114        let v = "x".repeat(78 - 3 - 1);
1115        let (w, left) = written_at("a", &v, 78);
1116        assert_eq!((w.len(), left), (78, 0));
1117        let (w, left) = written_at("a", &v, 10);
1118        assert_eq!((w.len(), left), (80, 1));
1119        // Too long for one line at all: sections, as `write_mime_param`.
1120        let long = "x".repeat(200);
1121        for room in [0, 10, 78] {
1122            let (w, left) = written_at("filename", &long, room);
1123            assert!(w.starts_with(";\r\n filename*0*=UTF-8''"), "{w}");
1124            let last = w.rsplit("\r\n").next().unwrap();
1125            assert_eq!(left, 78 - last.len());
1126        }
1127        // Calls chain on what is left.
1128        let mut buf = vec![0u8; 256];
1129        let mut out = crate::SliceSink::new(&mut buf);
1130        let mut room = 78 - "Content-Type: text/plain".len();
1131        for (n, v) in [
1132            ("charset", "utf-8"),
1133            ("format", "flowed"),
1134            ("delsp", "yes"),
1135            ("name", "a long attachment name.txt"),
1136        ] {
1137            room = write_mime_param_at(n, v, room, &mut out).unwrap();
1138        }
1139        assert_eq!(
1140            out.written(),
1141            b"; charset=utf-8; format=flowed; delsp=yes;\r\n name=\"a long attachment name.txt\""
1142        );
1143        assert_eq!(room, 78 - 34);
1144        assert_eq!(
1145            write_mime_param_at("a*", "x", 78, &mut crate::SliceSink::new(&mut [0u8; 8])),
1146            Err(WriteError::NotToken)
1147        );
1148    }
1149}