Skip to main content

parse_net/mail/
words.rs

1//! RFC 2047 encoded words in unstructured header text.
2
3use super::b64;
4use crate::{
5    class,
6    error::{ErrorKind, ParseError, Result, WriteError, err},
7    limits::Limits,
8    utf8::Utf8,
9    write::{CountSink, Sink, reserve},
10};
11
12/// The longest encoded word (RFC 2047 section 2).
13const WORD_LIMIT: usize = 75;
14/// `=?UTF-8?Q?` and `?=`.
15const OVERHEAD: usize = 12;
16
17/// Whether `b` is written as itself inside a Q-encoded word in a header
18/// (RFC 2047 section 5, rule 3).
19const fn q_plain(b: u8) -> bool {
20    b.is_ascii_alphanumeric() || matches!(b, b'!' | b'*' | b'+' | b'-' | b'/')
21}
22
23/// The Q-encoded length of every byte: 1 for a byte written as itself or
24/// as `_` (a space), 3 for `=XX`. One lookup instead of the class tests.
25// Built at compile time; `b` runs over 0..256, the array's own bounds.
26#[allow(clippy::indexing_slicing)]
27const Q_LEN: [u8; 256] = {
28    let mut t = [3u8; 256];
29    let mut b = 0usize;
30    while b < 256 {
31        if q_plain(b as u8) || b == b' ' as usize {
32            t[b] = 1;
33        }
34        b += 1;
35    }
36    t
37};
38
39/// The Q-encoded length of one byte.
40#[inline]
41fn q_byte_len(b: u8) -> usize {
42    usize::from(Q_LEN.get(usize::from(b)).copied().unwrap_or(3))
43}
44
45/// The length in bytes of the UTF-8 character that starts with `b`.
46const fn utf8_width(b: u8) -> usize {
47    match b {
48        0x00..=0x7f => 1,
49        0xc0..=0xdf => 2,
50        0xe0..=0xef => 3,
51        _ => 4,
52    }
53}
54
55/// The payload of one encoded word, built on the stack: at most 75 bytes
56/// less the 12 of `=?UTF-8?Q?` and `?=`.
57struct Payload {
58    buf: [u8; WORD_LIMIT - OVERHEAD],
59    len: usize,
60}
61
62impl Payload {
63    /// Appends the Q encoding of `ch` (one UTF-8 character); the caller has
64    /// checked that it fits.
65    fn push(&mut self, ch: &[u8]) {
66        const HEX: &[u8; 16] = b"0123456789ABCDEF";
67        for &b in ch {
68            let len = self.len;
69            if q_byte_len(b) == 1 {
70                if let Some(d) = self.buf.get_mut(len) {
71                    *d = if b == b' ' { b'_' } else { b };
72                }
73                self.len = len + 1;
74            } else {
75                let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
76                let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
77                if let Some([x, y, z]) = self.buf.get_mut(len..len + 3) {
78                    (*x, *y, *z) = (b'=', hi, lo);
79                }
80                self.len = len + 3;
81            }
82        }
83    }
84
85    fn as_bytes(&self) -> &[u8] {
86        self.buf.get(..self.len).unwrap_or(&[])
87    }
88}
89
90/// Whether `text` goes out as it is under the `B` encoder: printable ASCII
91/// without `=?` whose line stays legal. `put_words` keeps its own copy of
92/// this test: called from there, it cost the Q encoder 2% (callgrind).
93#[inline]
94fn goes_plain(text: &str, room_first_line: usize) -> bool {
95    let bytes = text.as_bytes();
96    let used = super::SOFT_LIMIT.saturating_sub(room_first_line);
97    bytes.iter().all(|b| (0x20..=0x7e).contains(b))
98        && !text.contains("=?")
99        && used + bytes.len() <= super::HARD_LIMIT
100}
101
102fn put_words(
103    text: &str,
104    room_first_line: usize,
105    out: &mut impl Sink,
106) -> core::result::Result<(), WriteError> {
107    let bytes = text.as_bytes();
108    // Plain text goes out as it is while its line stays legal; past that it
109    // is encoded, which breaks it into words.
110    let used = super::SOFT_LIMIT.saturating_sub(room_first_line);
111    if bytes.iter().all(|b| (0x20..=0x7e).contains(b))
112        && !text.contains("=?")
113        && used + bytes.len() <= super::HARD_LIMIT
114    {
115        out.put(bytes)?;
116        return Ok(());
117    }
118    let mut limit = room_first_line.min(WORD_LIMIT);
119    let mut word = Payload {
120        buf: [0; WORD_LIMIT - OVERHEAD],
121        len: 0,
122    };
123    let mut open = false;
124    let mut i = 0usize;
125    while let Some(&lead) = bytes.get(i) {
126        let ch = bytes.get(i..i + utf8_width(lead)).unwrap_or(&[]);
127        i += ch.len().max(1);
128        let mut n = 0usize;
129        for &b in ch {
130            n += q_byte_len(b);
131        }
132        if open && OVERHEAD + word.len + n > limit {
133            out.put(word.as_bytes())?;
134            out.put(b"?=\r\n ")?;
135            open = false;
136            limit = WORD_LIMIT;
137        }
138        if !open {
139            if OVERHEAD + n > limit {
140                // Not even one character fits in the room left on the first
141                // line: start on a new one.
142                out.put(b"\r\n ")?;
143                limit = WORD_LIMIT;
144            }
145            out.put(b"=?UTF-8?Q?")?;
146            open = true;
147            word.len = 0;
148        }
149        word.push(ch);
150    }
151    if open {
152        out.put(word.as_bytes())?;
153        out.put(b"?=")?;
154    }
155    Ok(())
156}
157
158/// Writes `text` for an unstructured header field: as it is when it is
159/// printable ASCII without `=?` that fits in a 998-byte line, otherwise as
160/// RFC 2047 `=?UTF-8?Q?...?=`
161/// words of at most 75 bytes, split only between characters, separated by
162/// CRLF SP. The first word fits in `room_first_line` (the columns left after
163/// `Name: `); when not even one character fits there, the words start on a
164/// new line.
165///
166/// # Errors
167///
168/// [`WriteError::Forbidden`] for a control character (C0, DEL, or C1);
169/// [`WriteError::Full`] (nothing written when the sink reports its room).
170///
171/// # Examples
172///
173/// ```
174/// use parse_net::{mail, SliceSink};
175///
176/// let mut buf = [0u8; 256];
177/// let mut out = SliceSink::new(&mut buf);
178/// mail::encode_words("Caf\u{e9} au lait", 64, &mut out).expect("room");
179/// assert_eq!(out.written(), b"=?UTF-8?Q?Caf=C3=A9_au_lait?=");
180/// ```
181pub fn encode_words(
182    text: &str,
183    room_first_line: usize,
184    out: &mut impl Sink,
185) -> core::result::Result<(), WriteError> {
186    if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
187        return Err(WriteError::forbidden(at));
188    }
189    let mut count = CountSink::new();
190    put_words(text, room_first_line, &mut count)?;
191    reserve(out, count.count())?;
192    put_words(text, room_first_line, out)
193}
194
195/// Writes one `=?UTF-8?B?...?=` word holding `chunk`, at most 45 bytes.
196fn put_b_word(chunk: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
197    let mut buf = [0u8; WORD_LIMIT - OVERHEAD];
198    let n = b64::encode(chunk, &mut buf);
199    out.put(b"=?UTF-8?B?")?;
200    out.put(buf.get(..n).unwrap_or(&[]))?;
201    out.put(b"?=")?;
202    Ok(())
203}
204
205fn put_words_b(
206    text: &str,
207    room_first_line: usize,
208    out: &mut impl Sink,
209) -> core::result::Result<(), WriteError> {
210    let bytes = text.as_bytes();
211    if goes_plain(text, room_first_line) {
212        out.put(bytes)?;
213        return Ok(());
214    }
215    let mut limit = room_first_line.min(WORD_LIMIT);
216    // The characters of the word being built: `bytes[start..start + len]`.
217    let (mut start, mut len) = (0usize, 0usize);
218    while let Some(&lead) = bytes.get(start + len) {
219        let w = utf8_width(lead);
220        if len > 0 && OVERHEAD + b64::encoded_len(len + w) > limit {
221            put_b_word(bytes.get(start..start + len).unwrap_or(&[]), out)?;
222            out.put(b"\r\n ")?;
223            (start, len, limit) = (start + len, 0, WORD_LIMIT);
224        }
225        if len == 0 && OVERHEAD + b64::encoded_len(w) > limit {
226            // Not even one character fits in the room left on the first
227            // line: start on a new one.
228            out.put(b"\r\n ")?;
229            limit = WORD_LIMIT;
230        }
231        len += w;
232    }
233    if len > 0 {
234        put_b_word(bytes.get(start..start + len).unwrap_or(&[]), out)?;
235    }
236    Ok(())
237}
238
239/// [`encode_words`] with the RFC 2047 `B` encoding: `=?UTF-8?B?...?=` words
240/// of at most 75 bytes, split only between characters, separated by CRLF SP.
241/// Text that goes out as it is under [`encode_words`] goes out as it is here.
242/// `B` costs four bytes for every three, `Q` three for every byte it escapes,
243/// so `B` is the shorter for text that is mostly not ASCII: a Japanese or
244/// Chinese subject is about 40% shorter. [`decode_words`] reads both.
245///
246/// # Errors
247///
248/// [`WriteError::Forbidden`] for a control character (C0, DEL, or C1);
249/// [`WriteError::Full`] (nothing written when the sink reports its room).
250///
251/// # Examples
252///
253/// ```
254/// use parse_net::{mail, Limits, SliceSink};
255///
256/// let mut buf = [0u8; 256];
257/// let mut out = SliceSink::new(&mut buf);
258/// mail::encode_words_b("\u{4f1a}\u{8b70}\u{306e}\u{304a}\u{77e5}\u{3089}\u{305b}", 64, &mut out).expect("room");
259/// assert_eq!(out.written(), b"=?UTF-8?B?5Lya6K2w44Gu44GK55+l44KJ44Gb?=");
260///
261/// let mut text = [0u8; 64];
262/// let s = mail::decode_words(out.written(), &Limits::DEFAULT, &mut text)?;
263/// assert_eq!(s, "\u{4f1a}\u{8b70}\u{306e}\u{304a}\u{77e5}\u{3089}\u{305b}");
264/// # Ok::<(), parse_net::ParseError>(())
265/// ```
266pub fn encode_words_b(
267    text: &str,
268    room_first_line: usize,
269    out: &mut impl Sink,
270) -> core::result::Result<(), WriteError> {
271    if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
272        return Err(WriteError::forbidden(at));
273    }
274    let mut count = CountSink::new();
275    put_words_b(text, room_first_line, &mut count)?;
276    reserve(out, count.count())?;
277    put_words_b(text, room_first_line, out)
278}
279
280/// The charsets decoded.
281#[derive(Clone, Copy, PartialEq, Eq)]
282enum Charset {
283    Utf8,
284    Ascii,
285    Latin1,
286}
287
288/// The parts of an encoded word.
289pub(super) struct Encoded<'a> {
290    charset: Charset,
291    /// The `B` encoding.
292    base64: bool,
293    text: &'a [u8],
294    /// The index after the closing `?=`.
295    pub(super) end: usize,
296}
297
298/// An encoded word at `v[i..]`: `=?charset?enc?text?=`, standing alone.
299/// `Ok(None)` when the bytes do not have that shape, or do not stand between
300/// whitespace: they are then ordinary text, whatever charset they name. Each
301/// scan stops at the first byte that cannot continue, so failed candidates
302/// never add up to a second pass over the value.
303pub(super) fn shape(v: &[u8], i: usize) -> Result<Option<Encoded<'_>>> {
304    let rest = v.get(i + 2..).unwrap_or(&[]);
305    let q1 = rest
306        .iter()
307        .position(|&b| !class::is_mime_token_char(b))
308        .unwrap_or(rest.len());
309    if q1 == 0 || rest.get(q1) != Some(&b'?') {
310        return Ok(None);
311    }
312    let charset = rest.get(..q1).unwrap_or(&[]);
313    let after = rest.get(q1 + 1..).unwrap_or(&[]);
314    let (Some(&enc), Some(b'?')) = (after.first(), after.get(1)) else {
315        return Ok(None);
316    };
317    let text_start = i + 2 + q1 + 3;
318    let text_rest = v.get(text_start..).unwrap_or(&[]);
319    let Some(end) = text_rest
320        .iter()
321        .position(|&b| matches!(b, b'?' | b' ' | b'\t'))
322    else {
323        return Ok(None);
324    };
325    if text_rest.get(end..end + 2) != Some(b"?=") {
326        return Ok(None);
327    }
328    let text = text_rest.get(..end).unwrap_or(&[]);
329    let after_word = text_start + end + 2;
330    if after_word != v.len() && ws_len(v, after_word) == 0 {
331        return Ok(None);
332    }
333    // RFC 2231 section 5: `charset*language`.
334    let name = charset.split(|&b| b == b'*').next().unwrap_or(&[]);
335    let cs = if name.eq_ignore_ascii_case(b"utf-8") {
336        Charset::Utf8
337    } else if name.eq_ignore_ascii_case(b"us-ascii") {
338        Charset::Ascii
339    } else if name.eq_ignore_ascii_case(b"iso-8859-1") {
340        Charset::Latin1
341    } else {
342        return err(ErrorKind::UnsupportedCharset, i + 2);
343    };
344    let b = match enc {
345        b'Q' | b'q' => false,
346        b'B' | b'b' => true,
347        _ => return err(ErrorKind::InvalidEncodedWord, i + 2 + q1 + 1),
348    };
349    Ok(Some(Encoded {
350        charset: cs,
351        base64: b,
352        text,
353        end: after_word,
354    }))
355}
356
357/// The length of the whitespace unit at `v[i]`: SP, HTAB, or a `CRLF WSP`
358/// fold; 0 for anything else.
359pub(super) fn ws_len(v: &[u8], i: usize) -> usize {
360    match (v.get(i), v.get(i + 1), v.get(i + 2)) {
361        (Some(b' ' | b'\t'), _, _) => 1,
362        (Some(b'\r'), Some(b'\n'), Some(b' ' | b'\t')) => 3,
363        _ => 0,
364    }
365}
366
367/// The text outside encoded words, checked as it is copied: UTF-8 (raw bytes
368/// above 0x7F are RFC 6532 text), with no control character but HTAB.
369pub(super) struct Text {
370    utf8: Utf8,
371    prev: u8,
372}
373
374impl Text {
375    /// A checker at a character boundary.
376    pub(super) const fn new() -> Self {
377        Self {
378            utf8: Utf8::new(),
379            prev: 0,
380        }
381    }
382
383    /// Whether the text so far ends on a character boundary.
384    pub(super) const fn is_complete(&self) -> bool {
385        self.utf8.is_complete()
386    }
387
388    pub(super) fn check(&mut self, b: u8, at: usize) -> Result<()> {
389        if b != b'\t' && crate::scan::is_control(b) {
390            return err(crate::scan::control_kind(b), at);
391        }
392        if self.prev == 0xc2 && (0x80..=0x9f).contains(&b) {
393            return err(ErrorKind::Control, at);
394        }
395        if !self.utf8.push(b) {
396            return err(ErrorKind::InvalidUtf8, at);
397        }
398        self.prev = b;
399        Ok(())
400    }
401
402    /// Copies a whitespace run, leaving out the CRLF of its folds.
403    fn run(&mut self, v: &[u8], s: usize, e: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
404        let mut i = s;
405        while i < e {
406            let n = ws_len(v, i);
407            let at = i + n - 1;
408            let wsp = v.get(at).copied().unwrap_or(b' ');
409            self.check(wsp, at)?;
410            emit(&[wsp]);
411            i += n;
412        }
413        Ok(())
414    }
415}
416
417/// The index of the `"` closing a quoted string whose body starts at `from`.
418fn closing_quote(v: &[u8], from: usize) -> Option<usize> {
419    let mut i = from;
420    loop {
421        match v.get(i)? {
422            b'\\' => i += 2,
423            b'"' => return Some(i),
424            _ => i += 1,
425        }
426    }
427}
428
429/// The checks every decoded byte meets, whichever the encoding, then its
430/// UTF-8 form emitted. `start` is the offset reported for it.
431#[inline(always)]
432fn put_decoded(
433    cs: Charset,
434    byte: u8,
435    prev: &mut u8,
436    utf8: &mut Utf8,
437    start: usize,
438    emit: &mut impl FnMut(&[u8]),
439) -> Result<()> {
440    // Every control character is refused, as in ext-values: C0 (HTAB
441    // included), DEL, and C1 (U+0080 to U+009F: `C2 80` to `C2 9F` in
442    // UTF-8, where 0xC2 is always a lead byte; bytes 0x80 to 0x9F in
443    // ISO-8859-1).
444    let c1 = match cs {
445        Charset::Utf8 => *prev == 0xc2 && (0x80..=0x9f).contains(&byte),
446        Charset::Latin1 => (0x80..=0x9f).contains(&byte),
447        Charset::Ascii => false,
448    };
449    if byte < 0x20 || byte == 0x7f || c1 {
450        return err(ErrorKind::ControlInDecoded, start);
451    }
452    *prev = byte;
453    match cs {
454        Charset::Utf8 => {
455            if !utf8.push(byte) {
456                return err(ErrorKind::InvalidUtf8, start);
457            }
458            emit(&[byte]);
459        }
460        Charset::Ascii if byte >= 0x80 => {
461            return err(ErrorKind::InvalidEncodedWord, start);
462        }
463        Charset::Ascii => emit(&[byte]),
464        Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
465        Charset::Latin1 => emit(&[byte]),
466    }
467    Ok(())
468}
469
470/// Decodes one B word's text, emitting UTF-8. The base64 is strict
471/// (`b64::decode`); each byte is then checked as a Q word's is, and reported
472/// at the group of four that holds it.
473fn decode_b(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
474    let mut bytes = [0u8; b64::MAX_DECODED];
475    let n =
476        b64::decode(text, &mut bytes).or_else(|i| err(ErrorKind::InvalidEncodedWord, at + i))?;
477    let decoded = bytes.get(..n).unwrap_or(&[]);
478    // A word that passes every check goes out in one piece. The checks are
479    // those of `put_decoded`, over the whole word at once: no C0 or DEL, no
480    // C1 (`C2 80` to `C2 9F`), whole UTF-8 characters; ASCII for the other
481    // charsets. Anything else takes the byte loop, which finds the error.
482    let ascii_text = |d: &[u8]| d.iter().all(|&b| (0x20..0x7f).contains(&b));
483    let clean = match cs {
484        Charset::Utf8 => {
485            decoded.iter().all(|&b| b >= 0x20 && b != 0x7f)
486                && !decoded
487                    .iter()
488                    .zip(decoded.iter().skip(1))
489                    .any(|(&a, &b)| a == 0xc2 && (0x80..=0x9f).contains(&b))
490                && core::str::from_utf8(decoded).is_ok()
491        }
492        Charset::Ascii | Charset::Latin1 => ascii_text(decoded),
493    };
494    if clean {
495        emit(decoded);
496        return Ok(());
497    }
498    let mut utf8 = Utf8::new();
499    let mut prev = 0u8;
500    for (k, &byte) in decoded.iter().enumerate() {
501        put_decoded(cs, byte, &mut prev, &mut utf8, at + k / 3 * 4, emit)?;
502    }
503    if !utf8.is_complete() {
504        // Each encoded word holds whole characters (RFC 2047 section 5).
505        return err(ErrorKind::InvalidUtf8, at + text.len());
506    }
507    Ok(())
508}
509
510/// Decodes one Q word's text, emitting UTF-8.
511fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
512    let mut utf8 = Utf8::new();
513    let (mut i, mut prev) = (0usize, 0u8);
514    while let Some(&b) = text.get(i) {
515        let start = at + i;
516        // A run of bytes written as themselves goes out whole: printable
517        // ASCII, which passes every check of `put_decoded` on a character
518        // boundary.
519        let plain = |c: u8| matches!(c, 0x21..=0x7e) && c != b'_' && c != b'=';
520        if plain(b) {
521            if !utf8.is_complete() {
522                return err(ErrorKind::InvalidUtf8, start);
523            }
524            let run = text.get(i..).unwrap_or(&[]);
525            let n = run.iter().position(|&c| !plain(c)).unwrap_or(run.len());
526            emit(run.get(..n).unwrap_or(&[]));
527            prev = run.get(n - 1).copied().unwrap_or(b);
528            i += n;
529            continue;
530        }
531        let byte = match b {
532            b'_' => b' ',
533            b'=' => {
534                let hi = text.get(i + 1).copied().and_then(class::hex_value);
535                let lo = text.get(i + 2).copied().and_then(class::hex_value);
536                let (Some(hi), Some(lo)) = (hi, lo) else {
537                    return err(ErrorKind::InvalidEncodedWord, start);
538                };
539                i += 2;
540                (hi << 4) | lo
541            }
542            0x21..=0x7e => b,
543            _ => return err(ErrorKind::InvalidEncodedWord, start),
544        };
545        i += 1;
546        put_decoded(cs, byte, &mut prev, &mut utf8, start, emit)?;
547    }
548    if !utf8.is_complete() {
549        // Each encoded word holds whole characters (RFC 2047 section 5).
550        return err(ErrorKind::InvalidUtf8, at + text.len());
551    }
552    Ok(())
553}
554
555/// Decodes `e`, an encoded word that starts at `start` in the value.
556pub(super) fn decode_word(
557    e: &Encoded<'_>,
558    start: usize,
559    emit: &mut impl FnMut(&[u8]),
560) -> Result<()> {
561    decode_encoded(e.charset, e.base64, e.text, start, e.end, emit)
562}
563
564fn decode_encoded(
565    cs: Charset,
566    b64: bool,
567    word: &[u8],
568    start: usize,
569    end: usize,
570    emit: &mut impl FnMut(&[u8]),
571) -> Result<()> {
572    let at = end - 2 - word.len();
573    if b64 {
574        // A `B` word keeps to the 75-byte limit of section 2, which also
575        // bounds what it decodes to (D-080).
576        if end - start > WORD_LIMIT {
577            return err(ErrorKind::InvalidEncodedWord, start);
578        }
579        decode_b(cs, word, at, emit)
580    } else {
581        decode_q(cs, word, at, emit)
582    }
583}
584
585/// Walks `v`, emitting the decoded text. Whitespace includes folds, whose
586/// CRLF is dropped, so the folded output of [`encode_words`] reads back. A
587/// quoted string starts at a word boundary and needs its closing `"`: a lone
588/// `"` (`5" disk`) is ordinary text. One pass: every scan ahead either
589/// consumes what it read or stops at the first byte that rules it out.
590fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
591    let mut text = Text::new();
592    let (mut i, mut words) = (0usize, 0usize);
593    let (mut boundary, mut after_word) = (true, false);
594    let mut held: Option<(usize, usize)> = None;
595    // Inside a quoted string while `i` is below this.
596    let mut quoted_until = 0usize;
597    // Cleared when no closing quote is left, so that scan is not repeated.
598    let mut closes = true;
599    while let Some(&b) = v.get(i) {
600        if i < quoted_until {
601            if ws_len(v, i) == 3 {
602                i += 2;
603                continue;
604            }
605            text.check(b, i)?;
606            emit(&[b]);
607            i += 1;
608            continue;
609        }
610        if ws_len(v, i) > 0 {
611            let start = i;
612            while ws_len(v, i) > 0 {
613                i += ws_len(v, i);
614            }
615            if after_word {
616                held = Some((start, i));
617            } else {
618                text.run(v, start, i, emit)?;
619            }
620            boundary = true;
621            continue;
622        }
623        if boundary && v.get(i..i + 2) == Some(b"=?") {
624            if let Some(Encoded {
625                charset: cs,
626                base64: b64,
627                text: word,
628                end,
629            }) = shape(v, i)?
630            {
631                if !text.utf8.is_complete() {
632                    return err(ErrorKind::InvalidUtf8, i);
633                }
634                words += 1;
635                if words > usize::from(limits.encoded_words) {
636                    return err(ErrorKind::TooManyMembers, i);
637                }
638                // Whitespace between two encoded words is dropped
639                // (RFC 2047 section 6.2).
640                held = None;
641                decode_encoded(cs, b64, word, i, end, emit)?;
642                after_word = true;
643                boundary = false;
644                i = end;
645                continue;
646            }
647        }
648        if let Some((s, e)) = held.take() {
649            text.run(v, s, e, emit)?;
650        }
651        after_word = false;
652        if b == b'"' && boundary && closes {
653            match closing_quote(v, i + 1) {
654                Some(close) => quoted_until = close + 1,
655                None => closes = false,
656            }
657        }
658        boundary = false;
659        text.check(b, i)?;
660        emit(&[b]);
661        i += 1;
662    }
663    if let Some((s, e)) = held {
664        text.run(v, s, e, emit)?;
665    }
666    if !text.utf8.is_complete() {
667        return err(ErrorKind::InvalidUtf8, v.len());
668    }
669    Ok(())
670}
671
672/// Decodes the RFC 2047 encoded words in unstructured header text into
673/// `out` and returns the text. Text outside encoded words is copied;
674/// whitespace between two adjacent encoded words is dropped (section 6.2);
675/// `_` is a space in Q; an encoded word must stand alone between whitespace,
676/// and words inside a quoted string are not decoded (section 5). Charsets:
677/// UTF-8, US-ASCII, ISO-8859-1 (transcoded), with an optional RFC 2231
678/// `*language`. `B` words are read strictly (D-080): whole groups of four,
679/// `=` padding only at the end with zero bits under it, no whitespace inside,
680/// and at most 75 bytes to a word. `CRLF WSP` folds count as whitespace, so
681/// the output of [`encode_words`] and [`encode_words_b`] reads back as
682/// written.
683///
684/// # Errors
685///
686/// [`ErrorKind::UnsupportedCharset`]; [`ErrorKind::InvalidEncodedWord`] (a
687/// bad encoding letter, a broken `=XX`, a byte not allowed in a Q word, base64
688/// that is not canonical, a `B` word longer than 75 bytes);
689/// [`ErrorKind::ControlInDecoded`] for a decoded control
690/// character (C0 including CR, LF, NUL, and HTAB; DEL; C1), as in ext-values;
691/// a control kind for a control byte outside the words, other than HTAB and
692/// the CRLF of a fold; [`ErrorKind::InvalidUtf8`] (a UTF-8 word that is not
693/// whole characters, or text outside the words that is not UTF-8);
694/// [`ErrorKind::TooManyMembers`]
695/// beyond `max_encoded_words`; [`ErrorKind::BufferTooSmall`]. Nothing is
696/// written on error.
697///
698/// # Examples
699///
700/// ```
701/// use parse_net::{mail, Limits};
702///
703/// let mut buf = [0u8; 64];
704/// let s = mail::decode_words(b"=?utf-8?q?Caf=C3=A9?= =?ISO-8859-1?Q?_cr=E8me?= now", &Limits::DEFAULT, &mut buf)?;
705/// assert_eq!(s, "Caf\u{e9} cr\u{e8}me now");
706/// # Ok::<(), parse_net::ParseError>(())
707/// ```
708pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
709    let mut len = 0usize;
710    walk(value, limits, &mut |b: &[u8]| len += b.len())?;
711    let Some(dst) = out.get_mut(..len) else {
712        return err(ErrorKind::BufferTooSmall, 0);
713    };
714    let mut n = 0usize;
715    walk(value, limits, &mut |b: &[u8]| {
716        if let Some(d) = dst.get_mut(n..n + b.len()) {
717            d.copy_from_slice(b);
718        }
719        n += b.len();
720    })?;
721    // The first walk checked the text; this is the safe conversion.
722    core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
723}
724
725#[cfg(test)]
726mod tests {
727    use super::*;
728    use std::{format, string::String, vec, vec::Vec};
729
730    fn enc(text: &str, room: usize) -> String {
731        let mut buf = vec![0u8; 2048];
732        let mut out = crate::SliceSink::new(&mut buf);
733        encode_words(text, room, &mut out).unwrap();
734        String::from_utf8(out.written().to_vec()).unwrap()
735    }
736
737    fn dec(v: &[u8]) -> Result<String> {
738        let mut buf = vec![0u8; 2048];
739        decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
740    }
741
742    #[test]
743    fn test_encode_words_edge_cases() {
744        assert_eq!(enc("plain ascii", 10), "plain ascii");
745        assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
746        let long = "\u{e9}".repeat(40);
747        let e = enc(&long, 75);
748        for word in e.split("\r\n ") {
749            assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
750            // Each word holds whole characters, so it decodes on its own.
751            assert!(dec(word.as_bytes()).is_ok());
752        }
753        assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
754        // An encoded word of exactly 75 bytes: 63 bytes of payload.
755        let e = enc(&"\u{20ac}".repeat(7), 75);
756        assert_eq!(e.len(), 12 + 63);
757        assert!(!e.starts_with("\r\n "));
758        assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
759        let mut buf = [0u8; 8];
760        let mut out = crate::SliceSink::new(&mut buf);
761        assert_eq!(
762            encode_words("a\nb", 70, &mut out),
763            Err(WriteError::Forbidden { at: 1 })
764        );
765        assert_eq!(
766            encode_words("a\u{85}", 70, &mut out),
767            Err(WriteError::Forbidden { at: 1 })
768        );
769        assert_eq!(
770            encode_words("\u{e9}\u{e9}", 70, &mut out),
771            Err(WriteError::Full)
772        );
773        assert!(out.is_empty());
774    }
775
776    #[test]
777    fn test_decode_words_edge_cases() {
778        assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
779        assert_eq!(dec(b"=?UTF-8?Q?a?=  \t =?UTF-8?Q?b?=").unwrap(), "ab");
780        assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
781        assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
782        assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
783        assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
784        assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
785        assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
786        assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
787        assert_eq!(
788            dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
789            ErrorKind::UnsupportedCharset
790        );
791        assert_eq!(
792            dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
793            ErrorKind::InvalidEncodedWord
794        );
795        assert_eq!(
796            dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
797            ErrorKind::InvalidEncodedWord
798        );
799        assert_eq!(dec(b"=?UTF-8?B?YQ==?=").unwrap(), "a");
800        assert_eq!(
801            dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
802            ErrorKind::ControlInDecoded
803        );
804        assert_eq!(
805            dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
806            ErrorKind::InvalidUtf8
807        );
808        assert_eq!(
809            dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
810            ErrorKind::InvalidEncodedWord
811        );
812        assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
813        let l = Limits::DEFAULT.max_encoded_words(2);
814        let mut buf = [0u8; 64];
815        assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
816        assert_eq!(
817            decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
818                .unwrap_err()
819                .kind(),
820            ErrorKind::TooManyMembers
821        );
822        let mut small = [0u8; 2];
823        assert_eq!(
824            decode_words(b"abc", &Limits::DEFAULT, &mut small)
825                .unwrap_err()
826                .kind(),
827            ErrorKind::BufferTooSmall
828        );
829        assert_eq!(small, [0, 0]);
830    }
831
832    fn enc_b(text: &str, room: usize) -> String {
833        let mut buf = vec![0u8; 4096];
834        let mut out = crate::SliceSink::new(&mut buf);
835        encode_words_b(text, room, &mut out).unwrap();
836        String::from_utf8(out.written().to_vec()).unwrap()
837    }
838
839    #[test]
840    fn test_decode_b_words() {
841        // RFC 2047 section 8, the first word of its `Subject` example (the
842        // second is ISO-8859-2, not a charset read here).
843        assert_eq!(
844            dec(b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=").unwrap(),
845            "If you can read this yo"
846        );
847        assert_eq!(
848            dec(b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r\n =?ISO-8859-1?Q?u?=")
849                .unwrap(),
850            "If you can read this you"
851        );
852        assert_eq!(
853            dec(b"=?utf-8?b?Q2Fmw6k=?= au lait").unwrap(),
854            "Caf\u{e9} au lait"
855        );
856        assert_eq!(dec(b"=?ISO-8859-1?B?6Q==?=").unwrap(), "\u{e9}");
857        assert_eq!(dec(b"=?UTF-8*ja?B?5Lya?=").unwrap(), "\u{4f1a}");
858        assert_eq!(dec(b"=?UTF-8?B??=").unwrap(), "");
859        // Strict base64, at the offset of the character refused.
860        let at = |v: &[u8]| {
861            let e = dec(v).unwrap_err();
862            (e.kind(), e.offset())
863        };
864        assert_eq!(at(b"=?UTF-8?B?YQ=?="), (ErrorKind::InvalidEncodedWord, 10));
865        assert_eq!(at(b"=?UTF-8?B?YR==?="), (ErrorKind::InvalidEncodedWord, 11));
866        assert_eq!(
867            at(b"=?UTF-8?B?YQ==YQ==?="),
868            (ErrorKind::InvalidEncodedWord, 12)
869        );
870        assert_eq!(at(b"=?UTF-8?B?Y-Q=?="), (ErrorKind::InvalidEncodedWord, 11));
871        assert_eq!(
872            at(b"=?UTF-8?B?YWJj\x80?="),
873            (ErrorKind::InvalidEncodedWord, 14)
874        );
875        // Whitespace ends the candidate: it is ordinary text, as in Q.
876        assert_eq!(dec(b"=?UTF-8?B?YW Jj?=").unwrap(), "=?UTF-8?B?YW Jj?=");
877        // Decoded bytes meet the Q rules, reported at their group of four.
878        assert_eq!(at(b"=?UTF-8?B?YQ0K?="), (ErrorKind::ControlInDecoded, 10));
879        assert_eq!(
880            at(b"=?UTF-8?B?YWJjGw==?="),
881            (ErrorKind::ControlInDecoded, 14)
882        );
883        assert_eq!(at(b"=?UTF-8?B?woU=?="), (ErrorKind::ControlInDecoded, 10));
884        assert_eq!(at(b"=?ISO-8859-1?B?hQ==?=").0, ErrorKind::ControlInDecoded);
885        assert_eq!(dec(b"=?UTF-8?B?w6k=?=x").unwrap(), "=?UTF-8?B?w6k=?=x");
886        assert_eq!(at(b"=?UTF-8?B?ww==?=").0, ErrorKind::InvalidUtf8);
887        assert_eq!(at(b"=?US-ASCII?B?6Q==?=").0, ErrorKind::InvalidEncodedWord);
888        // 75 bytes to a word: 64 base64 characters fit after `=?a?B?`.
889        let word = |n: usize| format!("=?UTF-8?B?{}?=", "QUFB".repeat(n));
890        assert_eq!(dec(word(15).as_bytes()).unwrap(), "A".repeat(45));
891        let long = format!("=?UTF-8?B?{}QQ==?=", "QUFB".repeat(15));
892        assert_eq!(long.len(), 76);
893        assert_eq!(at(long.as_bytes()), (ErrorKind::InvalidEncodedWord, 0));
894        assert_eq!(at(word(16).as_bytes()), (ErrorKind::InvalidEncodedWord, 0));
895    }
896
897    #[test]
898    fn test_encode_words_b_edge_cases() {
899        assert_eq!(enc_b("plain ascii", 10), "plain ascii");
900        assert_eq!(enc_b("a =?b", 70), "=?UTF-8?B?YSA9P2I=?=");
901        assert_eq!(enc_b("Caf\u{e9}", 70), "=?UTF-8?B?Q2Fmw6k=?=");
902        // Every word is at most 75 bytes, holds whole characters, and reads
903        // back on its own; the words together read back as the text.
904        let text = "\u{4f1a}\u{8b70}".repeat(30);
905        let e = enc_b(&text, 75);
906        for w in e.split("\r\n ") {
907            assert!(
908                w.len() <= 75 && w.starts_with("=?UTF-8?B?") && w.ends_with("?="),
909                "{w}"
910            );
911            assert!(dec(w.as_bytes()).is_ok());
912        }
913        assert_eq!(dec(e.as_bytes()).unwrap(), text);
914        // 45 bytes fill a word: 60 base64 characters, 72 bytes in all, as
915        // 63 is not a multiple of four.
916        let e = enc_b(&"\u{20ac}".repeat(15), 75);
917        assert_eq!(e.len(), 72);
918        let e = enc_b(&"\u{20ac}".repeat(16), 75);
919        assert_eq!(e.split("\r\n ").map(str::len).collect::<Vec<_>>(), [72, 16]);
920        // The first word fits the room left on the line, or starts a new one.
921        assert!(enc_b("\u{e9}", 16).starts_with("=?UTF-8?B?"));
922        assert!(enc_b("\u{e9}", 15).starts_with("\r\n =?UTF-8?B?"));
923        assert!(
924            enc_b(&"\u{e9}".repeat(20), 20)
925                .split("\r\n ")
926                .next()
927                .unwrap()
928                .len()
929                <= 20
930        );
931        // Mostly-CJK text: B is shorter than Q.
932        let subject = "\u{4f1a}\u{8b70}\u{306e}\u{304a}\u{77e5}\u{3089}\u{305b}";
933        assert!(enc_b(subject, 64).len() * 10 < enc(subject, 64).len() * 7);
934        let mut buf = [0u8; 8];
935        let mut out = crate::SliceSink::new(&mut buf);
936        assert_eq!(
937            encode_words_b("a\u{1b}", 70, &mut out),
938            Err(WriteError::Forbidden { at: 1 })
939        );
940        assert_eq!(
941            encode_words_b("\u{e9}\u{e9}", 70, &mut out),
942            Err(WriteError::Full)
943        );
944        assert!(out.is_empty());
945    }
946
947    proptest::proptest! {
948        #[test]
949        fn test_encode_words_b_round_trips(text in "[^\\p{Cc}]{0,120}", room in 0usize..80) {
950            let e = enc_b(&text, room);
951            for (i, w) in e.split("\r\n ").enumerate() {
952                if w.starts_with("=?") {
953                    proptest::prop_assert!(w.len() <= if i == 0 { room.min(75) } else { 75 }, "{w}");
954                }
955            }
956            // A fold before the first word follows `Name:` in the field, where
957            // a reader drops it with the whitespace after the colon.
958            let value = |s: &str| String::from(s.strip_prefix("\r\n ").unwrap_or(s));
959            proptest::prop_assert_eq!(&dec(value(&e).as_bytes()).unwrap(), &text);
960            // Q and B read back the same.
961            proptest::prop_assert_eq!(dec(value(&enc(&text, room)).as_bytes()).unwrap(), text);
962        }
963    }
964}