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.
289struct Encoded<'a> {
290    charset: Charset,
291    /// The `B` encoding.
292    base64: bool,
293    text: &'a [u8],
294    /// The index after the closing `?=`.
295    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.
303fn 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.
359fn 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.
369struct Text {
370    utf8: Utf8,
371    prev: u8,
372}
373
374impl Text {
375    fn check(&mut self, b: u8, at: usize) -> Result<()> {
376        if b != b'\t' && crate::scan::is_control(b) {
377            return err(crate::scan::control_kind(b), at);
378        }
379        if self.prev == 0xc2 && (0x80..=0x9f).contains(&b) {
380            return err(ErrorKind::Control, at);
381        }
382        if !self.utf8.push(b) {
383            return err(ErrorKind::InvalidUtf8, at);
384        }
385        self.prev = b;
386        Ok(())
387    }
388
389    /// Copies a whitespace run, leaving out the CRLF of its folds.
390    fn run(&mut self, v: &[u8], s: usize, e: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
391        let mut i = s;
392        while i < e {
393            let n = ws_len(v, i);
394            let at = i + n - 1;
395            let wsp = v.get(at).copied().unwrap_or(b' ');
396            self.check(wsp, at)?;
397            emit(&[wsp]);
398            i += n;
399        }
400        Ok(())
401    }
402}
403
404/// The index of the `"` closing a quoted string whose body starts at `from`.
405fn closing_quote(v: &[u8], from: usize) -> Option<usize> {
406    let mut i = from;
407    loop {
408        match v.get(i)? {
409            b'\\' => i += 2,
410            b'"' => return Some(i),
411            _ => i += 1,
412        }
413    }
414}
415
416/// The checks every decoded byte meets, whichever the encoding, then its
417/// UTF-8 form emitted. `start` is the offset reported for it.
418#[inline(always)]
419fn put_decoded(
420    cs: Charset,
421    byte: u8,
422    prev: &mut u8,
423    utf8: &mut Utf8,
424    start: usize,
425    emit: &mut impl FnMut(&[u8]),
426) -> Result<()> {
427    // Every control character is refused, as in ext-values: C0 (HTAB
428    // included), DEL, and C1 (U+0080 to U+009F: `C2 80` to `C2 9F` in
429    // UTF-8, where 0xC2 is always a lead byte; bytes 0x80 to 0x9F in
430    // ISO-8859-1).
431    let c1 = match cs {
432        Charset::Utf8 => *prev == 0xc2 && (0x80..=0x9f).contains(&byte),
433        Charset::Latin1 => (0x80..=0x9f).contains(&byte),
434        Charset::Ascii => false,
435    };
436    if byte < 0x20 || byte == 0x7f || c1 {
437        return err(ErrorKind::ControlInDecoded, start);
438    }
439    *prev = byte;
440    match cs {
441        Charset::Utf8 => {
442            if !utf8.push(byte) {
443                return err(ErrorKind::InvalidUtf8, start);
444            }
445            emit(&[byte]);
446        }
447        Charset::Ascii if byte >= 0x80 => {
448            return err(ErrorKind::InvalidEncodedWord, start);
449        }
450        Charset::Ascii => emit(&[byte]),
451        Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
452        Charset::Latin1 => emit(&[byte]),
453    }
454    Ok(())
455}
456
457/// Decodes one B word's text, emitting UTF-8. The base64 is strict
458/// (`b64::decode`); each byte is then checked as a Q word's is, and reported
459/// at the group of four that holds it.
460fn decode_b(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
461    let mut bytes = [0u8; b64::MAX_DECODED];
462    let n =
463        b64::decode(text, &mut bytes).or_else(|i| err(ErrorKind::InvalidEncodedWord, at + i))?;
464    let decoded = bytes.get(..n).unwrap_or(&[]);
465    // A word that passes every check goes out in one piece. The checks are
466    // those of `put_decoded`, over the whole word at once: no C0 or DEL, no
467    // C1 (`C2 80` to `C2 9F`), whole UTF-8 characters; ASCII for the other
468    // charsets. Anything else takes the byte loop, which finds the error.
469    let ascii_text = |d: &[u8]| d.iter().all(|&b| (0x20..0x7f).contains(&b));
470    let clean = match cs {
471        Charset::Utf8 => {
472            decoded.iter().all(|&b| b >= 0x20 && b != 0x7f)
473                && !decoded
474                    .iter()
475                    .zip(decoded.iter().skip(1))
476                    .any(|(&a, &b)| a == 0xc2 && (0x80..=0x9f).contains(&b))
477                && core::str::from_utf8(decoded).is_ok()
478        }
479        Charset::Ascii | Charset::Latin1 => ascii_text(decoded),
480    };
481    if clean {
482        emit(decoded);
483        return Ok(());
484    }
485    let mut utf8 = Utf8::new();
486    let mut prev = 0u8;
487    for (k, &byte) in decoded.iter().enumerate() {
488        put_decoded(cs, byte, &mut prev, &mut utf8, at + k / 3 * 4, emit)?;
489    }
490    if !utf8.is_complete() {
491        // Each encoded word holds whole characters (RFC 2047 section 5).
492        return err(ErrorKind::InvalidUtf8, at + text.len());
493    }
494    Ok(())
495}
496
497/// Decodes one Q word's text, emitting UTF-8.
498fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
499    let mut utf8 = Utf8::new();
500    let (mut i, mut prev) = (0usize, 0u8);
501    while let Some(&b) = text.get(i) {
502        let start = at + i;
503        let byte = match b {
504            b'_' => b' ',
505            b'=' => {
506                let hi = text.get(i + 1).copied().and_then(class::hex_value);
507                let lo = text.get(i + 2).copied().and_then(class::hex_value);
508                let (Some(hi), Some(lo)) = (hi, lo) else {
509                    return err(ErrorKind::InvalidEncodedWord, start);
510                };
511                i += 2;
512                (hi << 4) | lo
513            }
514            0x21..=0x7e => b,
515            _ => return err(ErrorKind::InvalidEncodedWord, start),
516        };
517        i += 1;
518        put_decoded(cs, byte, &mut prev, &mut utf8, start, emit)?;
519    }
520    if !utf8.is_complete() {
521        // Each encoded word holds whole characters (RFC 2047 section 5).
522        return err(ErrorKind::InvalidUtf8, at + text.len());
523    }
524    Ok(())
525}
526
527/// Walks `v`, emitting the decoded text. Whitespace includes folds, whose
528/// CRLF is dropped, so the folded output of [`encode_words`] reads back. A
529/// quoted string starts at a word boundary and needs its closing `"`: a lone
530/// `"` (`5" disk`) is ordinary text. One pass: every scan ahead either
531/// consumes what it read or stops at the first byte that rules it out.
532fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
533    let mut text = Text {
534        utf8: Utf8::new(),
535        prev: 0,
536    };
537    let (mut i, mut words) = (0usize, 0usize);
538    let (mut boundary, mut after_word) = (true, false);
539    let mut held: Option<(usize, usize)> = None;
540    // Inside a quoted string while `i` is below this.
541    let mut quoted_until = 0usize;
542    // Cleared when no closing quote is left, so that scan is not repeated.
543    let mut closes = true;
544    while let Some(&b) = v.get(i) {
545        if i < quoted_until {
546            if ws_len(v, i) == 3 {
547                i += 2;
548                continue;
549            }
550            text.check(b, i)?;
551            emit(&[b]);
552            i += 1;
553            continue;
554        }
555        if ws_len(v, i) > 0 {
556            let start = i;
557            while ws_len(v, i) > 0 {
558                i += ws_len(v, i);
559            }
560            if after_word {
561                held = Some((start, i));
562            } else {
563                text.run(v, start, i, emit)?;
564            }
565            boundary = true;
566            continue;
567        }
568        if boundary && v.get(i..i + 2) == Some(b"=?") {
569            if let Some(Encoded {
570                charset: cs,
571                base64: b64,
572                text: word,
573                end,
574            }) = shape(v, i)?
575            {
576                if !text.utf8.is_complete() {
577                    return err(ErrorKind::InvalidUtf8, i);
578                }
579                words += 1;
580                if words > usize::from(limits.encoded_words) {
581                    return err(ErrorKind::TooManyMembers, i);
582                }
583                // Whitespace between two encoded words is dropped
584                // (RFC 2047 section 6.2).
585                held = None;
586                let at = end - 2 - word.len();
587                if b64 {
588                    // A `B` word keeps to the 75-byte limit of section 2,
589                    // which also bounds what it decodes to (D-080).
590                    if end - i > WORD_LIMIT {
591                        return err(ErrorKind::InvalidEncodedWord, i);
592                    }
593                    decode_b(cs, word, at, emit)?;
594                } else {
595                    decode_q(cs, word, at, emit)?;
596                }
597                after_word = true;
598                boundary = false;
599                i = end;
600                continue;
601            }
602        }
603        if let Some((s, e)) = held.take() {
604            text.run(v, s, e, emit)?;
605        }
606        after_word = false;
607        if b == b'"' && boundary && closes {
608            match closing_quote(v, i + 1) {
609                Some(close) => quoted_until = close + 1,
610                None => closes = false,
611            }
612        }
613        boundary = false;
614        text.check(b, i)?;
615        emit(&[b]);
616        i += 1;
617    }
618    if let Some((s, e)) = held {
619        text.run(v, s, e, emit)?;
620    }
621    if !text.utf8.is_complete() {
622        return err(ErrorKind::InvalidUtf8, v.len());
623    }
624    Ok(())
625}
626
627/// Decodes the RFC 2047 encoded words in unstructured header text into
628/// `out` and returns the text. Text outside encoded words is copied;
629/// whitespace between two adjacent encoded words is dropped (section 6.2);
630/// `_` is a space in Q; an encoded word must stand alone between whitespace,
631/// and words inside a quoted string are not decoded (section 5). Charsets:
632/// UTF-8, US-ASCII, ISO-8859-1 (transcoded), with an optional RFC 2231
633/// `*language`. `B` words are read strictly (D-080): whole groups of four,
634/// `=` padding only at the end with zero bits under it, no whitespace inside,
635/// and at most 75 bytes to a word. `CRLF WSP` folds count as whitespace, so
636/// the output of [`encode_words`] and [`encode_words_b`] reads back as
637/// written.
638///
639/// # Errors
640///
641/// [`ErrorKind::UnsupportedCharset`]; [`ErrorKind::InvalidEncodedWord`] (a
642/// bad encoding letter, a broken `=XX`, a byte not allowed in a Q word, base64
643/// that is not canonical, a `B` word longer than 75 bytes);
644/// [`ErrorKind::ControlInDecoded`] for a decoded control
645/// character (C0 including CR, LF, NUL, and HTAB; DEL; C1), as in ext-values;
646/// a control kind for a control byte outside the words, other than HTAB and
647/// the CRLF of a fold; [`ErrorKind::InvalidUtf8`] (a UTF-8 word that is not
648/// whole characters, or text outside the words that is not UTF-8);
649/// [`ErrorKind::TooManyMembers`]
650/// beyond `max_encoded_words`; [`ErrorKind::BufferTooSmall`]. Nothing is
651/// written on error.
652///
653/// # Examples
654///
655/// ```
656/// use parse_net::{mail, Limits};
657///
658/// let mut buf = [0u8; 64];
659/// let s = mail::decode_words(b"=?utf-8?q?Caf=C3=A9?= =?ISO-8859-1?Q?_cr=E8me?= now", &Limits::DEFAULT, &mut buf)?;
660/// assert_eq!(s, "Caf\u{e9} cr\u{e8}me now");
661/// # Ok::<(), parse_net::ParseError>(())
662/// ```
663pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
664    let mut len = 0usize;
665    walk(value, limits, &mut |b: &[u8]| len += b.len())?;
666    let Some(dst) = out.get_mut(..len) else {
667        return err(ErrorKind::BufferTooSmall, 0);
668    };
669    let mut n = 0usize;
670    walk(value, limits, &mut |b: &[u8]| {
671        if let Some(d) = dst.get_mut(n..n + b.len()) {
672            d.copy_from_slice(b);
673        }
674        n += b.len();
675    })?;
676    // The first walk checked the text; this is the safe conversion.
677    core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
678}
679
680#[cfg(test)]
681mod tests {
682    use super::*;
683    use std::{format, string::String, vec, vec::Vec};
684
685    fn enc(text: &str, room: usize) -> String {
686        let mut buf = vec![0u8; 2048];
687        let mut out = crate::SliceSink::new(&mut buf);
688        encode_words(text, room, &mut out).unwrap();
689        String::from_utf8(out.written().to_vec()).unwrap()
690    }
691
692    fn dec(v: &[u8]) -> Result<String> {
693        let mut buf = vec![0u8; 2048];
694        decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
695    }
696
697    #[test]
698    fn test_encode_words_edge_cases() {
699        assert_eq!(enc("plain ascii", 10), "plain ascii");
700        assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
701        let long = "\u{e9}".repeat(40);
702        let e = enc(&long, 75);
703        for word in e.split("\r\n ") {
704            assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
705            // Each word holds whole characters, so it decodes on its own.
706            assert!(dec(word.as_bytes()).is_ok());
707        }
708        assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
709        // An encoded word of exactly 75 bytes: 63 bytes of payload.
710        let e = enc(&"\u{20ac}".repeat(7), 75);
711        assert_eq!(e.len(), 12 + 63);
712        assert!(!e.starts_with("\r\n "));
713        assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
714        let mut buf = [0u8; 8];
715        let mut out = crate::SliceSink::new(&mut buf);
716        assert_eq!(
717            encode_words("a\nb", 70, &mut out),
718            Err(WriteError::Forbidden { at: 1 })
719        );
720        assert_eq!(
721            encode_words("a\u{85}", 70, &mut out),
722            Err(WriteError::Forbidden { at: 1 })
723        );
724        assert_eq!(
725            encode_words("\u{e9}\u{e9}", 70, &mut out),
726            Err(WriteError::Full)
727        );
728        assert!(out.is_empty());
729    }
730
731    #[test]
732    fn test_decode_words_edge_cases() {
733        assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
734        assert_eq!(dec(b"=?UTF-8?Q?a?=  \t =?UTF-8?Q?b?=").unwrap(), "ab");
735        assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
736        assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
737        assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
738        assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
739        assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
740        assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
741        assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
742        assert_eq!(
743            dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
744            ErrorKind::UnsupportedCharset
745        );
746        assert_eq!(
747            dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
748            ErrorKind::InvalidEncodedWord
749        );
750        assert_eq!(
751            dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
752            ErrorKind::InvalidEncodedWord
753        );
754        assert_eq!(dec(b"=?UTF-8?B?YQ==?=").unwrap(), "a");
755        assert_eq!(
756            dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
757            ErrorKind::ControlInDecoded
758        );
759        assert_eq!(
760            dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
761            ErrorKind::InvalidUtf8
762        );
763        assert_eq!(
764            dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
765            ErrorKind::InvalidEncodedWord
766        );
767        assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
768        let l = Limits::DEFAULT.max_encoded_words(2);
769        let mut buf = [0u8; 64];
770        assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
771        assert_eq!(
772            decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
773                .unwrap_err()
774                .kind(),
775            ErrorKind::TooManyMembers
776        );
777        let mut small = [0u8; 2];
778        assert_eq!(
779            decode_words(b"abc", &Limits::DEFAULT, &mut small)
780                .unwrap_err()
781                .kind(),
782            ErrorKind::BufferTooSmall
783        );
784        assert_eq!(small, [0, 0]);
785    }
786
787    fn enc_b(text: &str, room: usize) -> String {
788        let mut buf = vec![0u8; 4096];
789        let mut out = crate::SliceSink::new(&mut buf);
790        encode_words_b(text, room, &mut out).unwrap();
791        String::from_utf8(out.written().to_vec()).unwrap()
792    }
793
794    #[test]
795    fn test_decode_b_words() {
796        // RFC 2047 section 8, the first word of its `Subject` example (the
797        // second is ISO-8859-2, not a charset read here).
798        assert_eq!(
799            dec(b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=").unwrap(),
800            "If you can read this yo"
801        );
802        assert_eq!(
803            dec(b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r\n =?ISO-8859-1?Q?u?=")
804                .unwrap(),
805            "If you can read this you"
806        );
807        assert_eq!(
808            dec(b"=?utf-8?b?Q2Fmw6k=?= au lait").unwrap(),
809            "Caf\u{e9} au lait"
810        );
811        assert_eq!(dec(b"=?ISO-8859-1?B?6Q==?=").unwrap(), "\u{e9}");
812        assert_eq!(dec(b"=?UTF-8*ja?B?5Lya?=").unwrap(), "\u{4f1a}");
813        assert_eq!(dec(b"=?UTF-8?B??=").unwrap(), "");
814        // Strict base64, at the offset of the character refused.
815        let at = |v: &[u8]| {
816            let e = dec(v).unwrap_err();
817            (e.kind(), e.offset())
818        };
819        assert_eq!(at(b"=?UTF-8?B?YQ=?="), (ErrorKind::InvalidEncodedWord, 10));
820        assert_eq!(at(b"=?UTF-8?B?YR==?="), (ErrorKind::InvalidEncodedWord, 11));
821        assert_eq!(
822            at(b"=?UTF-8?B?YQ==YQ==?="),
823            (ErrorKind::InvalidEncodedWord, 12)
824        );
825        assert_eq!(at(b"=?UTF-8?B?Y-Q=?="), (ErrorKind::InvalidEncodedWord, 11));
826        assert_eq!(
827            at(b"=?UTF-8?B?YWJj\x80?="),
828            (ErrorKind::InvalidEncodedWord, 14)
829        );
830        // Whitespace ends the candidate: it is ordinary text, as in Q.
831        assert_eq!(dec(b"=?UTF-8?B?YW Jj?=").unwrap(), "=?UTF-8?B?YW Jj?=");
832        // Decoded bytes meet the Q rules, reported at their group of four.
833        assert_eq!(at(b"=?UTF-8?B?YQ0K?="), (ErrorKind::ControlInDecoded, 10));
834        assert_eq!(
835            at(b"=?UTF-8?B?YWJjGw==?="),
836            (ErrorKind::ControlInDecoded, 14)
837        );
838        assert_eq!(at(b"=?UTF-8?B?woU=?="), (ErrorKind::ControlInDecoded, 10));
839        assert_eq!(at(b"=?ISO-8859-1?B?hQ==?=").0, ErrorKind::ControlInDecoded);
840        assert_eq!(dec(b"=?UTF-8?B?w6k=?=x").unwrap(), "=?UTF-8?B?w6k=?=x");
841        assert_eq!(at(b"=?UTF-8?B?ww==?=").0, ErrorKind::InvalidUtf8);
842        assert_eq!(at(b"=?US-ASCII?B?6Q==?=").0, ErrorKind::InvalidEncodedWord);
843        // 75 bytes to a word: 64 base64 characters fit after `=?a?B?`.
844        let word = |n: usize| format!("=?UTF-8?B?{}?=", "QUFB".repeat(n));
845        assert_eq!(dec(word(15).as_bytes()).unwrap(), "A".repeat(45));
846        let long = format!("=?UTF-8?B?{}QQ==?=", "QUFB".repeat(15));
847        assert_eq!(long.len(), 76);
848        assert_eq!(at(long.as_bytes()), (ErrorKind::InvalidEncodedWord, 0));
849        assert_eq!(at(word(16).as_bytes()), (ErrorKind::InvalidEncodedWord, 0));
850    }
851
852    #[test]
853    fn test_encode_words_b_edge_cases() {
854        assert_eq!(enc_b("plain ascii", 10), "plain ascii");
855        assert_eq!(enc_b("a =?b", 70), "=?UTF-8?B?YSA9P2I=?=");
856        assert_eq!(enc_b("Caf\u{e9}", 70), "=?UTF-8?B?Q2Fmw6k=?=");
857        // Every word is at most 75 bytes, holds whole characters, and reads
858        // back on its own; the words together read back as the text.
859        let text = "\u{4f1a}\u{8b70}".repeat(30);
860        let e = enc_b(&text, 75);
861        for w in e.split("\r\n ") {
862            assert!(
863                w.len() <= 75 && w.starts_with("=?UTF-8?B?") && w.ends_with("?="),
864                "{w}"
865            );
866            assert!(dec(w.as_bytes()).is_ok());
867        }
868        assert_eq!(dec(e.as_bytes()).unwrap(), text);
869        // 45 bytes fill a word: 60 base64 characters, 72 bytes in all, as
870        // 63 is not a multiple of four.
871        let e = enc_b(&"\u{20ac}".repeat(15), 75);
872        assert_eq!(e.len(), 72);
873        let e = enc_b(&"\u{20ac}".repeat(16), 75);
874        assert_eq!(e.split("\r\n ").map(str::len).collect::<Vec<_>>(), [72, 16]);
875        // The first word fits the room left on the line, or starts a new one.
876        assert!(enc_b("\u{e9}", 16).starts_with("=?UTF-8?B?"));
877        assert!(enc_b("\u{e9}", 15).starts_with("\r\n =?UTF-8?B?"));
878        assert!(
879            enc_b(&"\u{e9}".repeat(20), 20)
880                .split("\r\n ")
881                .next()
882                .unwrap()
883                .len()
884                <= 20
885        );
886        // Mostly-CJK text: B is shorter than Q.
887        let subject = "\u{4f1a}\u{8b70}\u{306e}\u{304a}\u{77e5}\u{3089}\u{305b}";
888        assert!(enc_b(subject, 64).len() * 10 < enc(subject, 64).len() * 7);
889        let mut buf = [0u8; 8];
890        let mut out = crate::SliceSink::new(&mut buf);
891        assert_eq!(
892            encode_words_b("a\u{1b}", 70, &mut out),
893            Err(WriteError::Forbidden { at: 1 })
894        );
895        assert_eq!(
896            encode_words_b("\u{e9}\u{e9}", 70, &mut out),
897            Err(WriteError::Full)
898        );
899        assert!(out.is_empty());
900    }
901
902    proptest::proptest! {
903        #[test]
904        fn test_encode_words_b_round_trips(text in "[^\\p{Cc}]{0,120}", room in 0usize..80) {
905            let e = enc_b(&text, room);
906            for (i, w) in e.split("\r\n ").enumerate() {
907                if w.starts_with("=?") {
908                    proptest::prop_assert!(w.len() <= if i == 0 { room.min(75) } else { 75 }, "{w}");
909                }
910            }
911            // A fold before the first word follows `Name:` in the field, where
912            // a reader drops it with the whitespace after the colon.
913            let value = |s: &str| String::from(s.strip_prefix("\r\n ").unwrap_or(s));
914            proptest::prop_assert_eq!(&dec(value(&e).as_bytes()).unwrap(), &text);
915            // Q and B read back the same.
916            proptest::prop_assert_eq!(dec(value(&enc(&text, room)).as_bytes()).unwrap(), text);
917        }
918    }
919}