Skip to main content

parse_net/mail/
words.rs

1//! RFC 2047 encoded words in unstructured header text.
2
3use crate::{
4    class,
5    error::{ErrorKind, ParseError, Result, WriteError, err},
6    limits::Limits,
7    utf8::Utf8,
8    write::{CountSink, Sink, reserve},
9};
10
11/// The longest encoded word (RFC 2047 section 2).
12const WORD_LIMIT: usize = 75;
13/// `=?UTF-8?Q?` and `?=`.
14const OVERHEAD: usize = 12;
15
16/// Whether `b` is written as itself inside a Q-encoded word in a header
17/// (RFC 2047 section 5, rule 3).
18const fn q_plain(b: u8) -> bool {
19    b.is_ascii_alphanumeric() || matches!(b, b'!' | b'*' | b'+' | b'-' | b'/')
20}
21
22/// The Q-encoded length of every byte: 1 for a byte written as itself or
23/// as `_` (a space), 3 for `=XX`. One lookup instead of the class tests.
24// Built at compile time; `b` runs over 0..256, the array's own bounds.
25#[allow(clippy::indexing_slicing)]
26const Q_LEN: [u8; 256] = {
27    let mut t = [3u8; 256];
28    let mut b = 0usize;
29    while b < 256 {
30        if q_plain(b as u8) || b == b' ' as usize {
31            t[b] = 1;
32        }
33        b += 1;
34    }
35    t
36};
37
38/// The Q-encoded length of one byte.
39#[inline]
40fn q_byte_len(b: u8) -> usize {
41    usize::from(Q_LEN.get(usize::from(b)).copied().unwrap_or(3))
42}
43
44/// The length in bytes of the UTF-8 character that starts with `b`.
45const fn utf8_width(b: u8) -> usize {
46    match b {
47        0x00..=0x7f => 1,
48        0xc0..=0xdf => 2,
49        0xe0..=0xef => 3,
50        _ => 4,
51    }
52}
53
54/// The payload of one encoded word, built on the stack: at most 75 bytes
55/// less the 12 of `=?UTF-8?Q?` and `?=`.
56struct Payload {
57    buf: [u8; WORD_LIMIT - OVERHEAD],
58    len: usize,
59}
60
61impl Payload {
62    /// Appends the Q encoding of `ch` (one UTF-8 character); the caller has
63    /// checked that it fits.
64    fn push(&mut self, ch: &[u8]) {
65        const HEX: &[u8; 16] = b"0123456789ABCDEF";
66        for &b in ch {
67            let len = self.len;
68            if q_byte_len(b) == 1 {
69                if let Some(d) = self.buf.get_mut(len) {
70                    *d = if b == b' ' { b'_' } else { b };
71                }
72                self.len = len + 1;
73            } else {
74                let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
75                let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
76                if let Some([x, y, z]) = self.buf.get_mut(len..len + 3) {
77                    (*x, *y, *z) = (b'=', hi, lo);
78                }
79                self.len = len + 3;
80            }
81        }
82    }
83
84    fn as_bytes(&self) -> &[u8] {
85        self.buf.get(..self.len).unwrap_or(&[])
86    }
87}
88
89fn put_words(
90    text: &str,
91    room_first_line: usize,
92    out: &mut impl Sink,
93) -> core::result::Result<(), WriteError> {
94    let bytes = text.as_bytes();
95    // Plain text goes out as it is while its line stays legal; past that it
96    // is encoded, which breaks it into words.
97    let used = super::SOFT_LIMIT.saturating_sub(room_first_line);
98    if bytes.iter().all(|b| (0x20..=0x7e).contains(b))
99        && !text.contains("=?")
100        && used + bytes.len() <= super::HARD_LIMIT
101    {
102        out.put(bytes)?;
103        return Ok(());
104    }
105    let mut limit = room_first_line.min(WORD_LIMIT);
106    let mut word = Payload {
107        buf: [0; WORD_LIMIT - OVERHEAD],
108        len: 0,
109    };
110    let mut open = false;
111    let mut i = 0usize;
112    while let Some(&lead) = bytes.get(i) {
113        let ch = bytes.get(i..i + utf8_width(lead)).unwrap_or(&[]);
114        i += ch.len().max(1);
115        let mut n = 0usize;
116        for &b in ch {
117            n += q_byte_len(b);
118        }
119        if open && OVERHEAD + word.len + n > limit {
120            out.put(word.as_bytes())?;
121            out.put(b"?=\r\n ")?;
122            open = false;
123            limit = WORD_LIMIT;
124        }
125        if !open {
126            if OVERHEAD + n > limit {
127                // Not even one character fits in the room left on the first
128                // line: start on a new one.
129                out.put(b"\r\n ")?;
130                limit = WORD_LIMIT;
131            }
132            out.put(b"=?UTF-8?Q?")?;
133            open = true;
134            word.len = 0;
135        }
136        word.push(ch);
137    }
138    if open {
139        out.put(word.as_bytes())?;
140        out.put(b"?=")?;
141    }
142    Ok(())
143}
144
145/// Writes `text` for an unstructured header field: as it is when it is
146/// printable ASCII without `=?` that fits in a 998-byte line, otherwise as
147/// RFC 2047 `=?UTF-8?Q?...?=`
148/// words of at most 75 bytes, split only between characters, separated by
149/// CRLF SP. The first word fits in `room_first_line` (the columns left after
150/// `Name: `); when not even one character fits there, the words start on a
151/// new line.
152///
153/// # Errors
154///
155/// [`WriteError::Forbidden`] for a control character (C0, DEL, or C1);
156/// [`WriteError::Full`] (nothing written when the sink reports its room).
157///
158/// # Examples
159///
160/// ```
161/// use parse_net::{mail, SliceSink};
162///
163/// let mut buf = [0u8; 256];
164/// let mut out = SliceSink::new(&mut buf);
165/// mail::encode_words("Caf\u{e9} au lait", 64, &mut out).expect("room");
166/// assert_eq!(out.written(), b"=?UTF-8?Q?Caf=C3=A9_au_lait?=");
167/// ```
168pub fn encode_words(
169    text: &str,
170    room_first_line: usize,
171    out: &mut impl Sink,
172) -> core::result::Result<(), WriteError> {
173    if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
174        return Err(WriteError::forbidden(at));
175    }
176    let mut count = CountSink::new();
177    put_words(text, room_first_line, &mut count)?;
178    reserve(out, count.count())?;
179    put_words(text, room_first_line, out)
180}
181
182/// The charsets decoded.
183#[derive(Clone, Copy, PartialEq, Eq)]
184enum Charset {
185    Utf8,
186    Ascii,
187    Latin1,
188}
189
190/// The parts of an encoded word.
191struct Encoded<'a> {
192    charset: Charset,
193    /// The `B` encoding.
194    base64: bool,
195    text: &'a [u8],
196    /// The index after the closing `?=`.
197    end: usize,
198}
199
200/// An encoded word at `v[i..]`: `=?charset?enc?text?=`, standing alone.
201/// `Ok(None)` when the bytes do not have that shape, or do not stand between
202/// whitespace: they are then ordinary text, whatever charset they name. Each
203/// scan stops at the first byte that cannot continue, so failed candidates
204/// never add up to a second pass over the value.
205fn shape(v: &[u8], i: usize) -> Result<Option<Encoded<'_>>> {
206    let rest = v.get(i + 2..).unwrap_or(&[]);
207    let q1 = rest
208        .iter()
209        .position(|&b| !class::is_mime_token_char(b))
210        .unwrap_or(rest.len());
211    if q1 == 0 || rest.get(q1) != Some(&b'?') {
212        return Ok(None);
213    }
214    let charset = rest.get(..q1).unwrap_or(&[]);
215    let after = rest.get(q1 + 1..).unwrap_or(&[]);
216    let (Some(&enc), Some(b'?')) = (after.first(), after.get(1)) else {
217        return Ok(None);
218    };
219    let text_start = i + 2 + q1 + 3;
220    let text_rest = v.get(text_start..).unwrap_or(&[]);
221    let Some(end) = text_rest
222        .iter()
223        .position(|&b| matches!(b, b'?' | b' ' | b'\t'))
224    else {
225        return Ok(None);
226    };
227    if text_rest.get(end..end + 2) != Some(b"?=") {
228        return Ok(None);
229    }
230    let text = text_rest.get(..end).unwrap_or(&[]);
231    let after_word = text_start + end + 2;
232    if after_word != v.len() && ws_len(v, after_word) == 0 {
233        return Ok(None);
234    }
235    // RFC 2231 section 5: `charset*language`.
236    let name = charset.split(|&b| b == b'*').next().unwrap_or(&[]);
237    let cs = if name.eq_ignore_ascii_case(b"utf-8") {
238        Charset::Utf8
239    } else if name.eq_ignore_ascii_case(b"us-ascii") {
240        Charset::Ascii
241    } else if name.eq_ignore_ascii_case(b"iso-8859-1") {
242        Charset::Latin1
243    } else {
244        return err(ErrorKind::UnsupportedCharset, i + 2);
245    };
246    let b = match enc {
247        b'Q' | b'q' => false,
248        b'B' | b'b' => true,
249        _ => return err(ErrorKind::InvalidEncodedWord, i + 2 + q1 + 1),
250    };
251    Ok(Some(Encoded {
252        charset: cs,
253        base64: b,
254        text,
255        end: after_word,
256    }))
257}
258
259/// The length of the whitespace unit at `v[i]`: SP, HTAB, or a `CRLF WSP`
260/// fold; 0 for anything else.
261fn ws_len(v: &[u8], i: usize) -> usize {
262    match (v.get(i), v.get(i + 1), v.get(i + 2)) {
263        (Some(b' ' | b'\t'), _, _) => 1,
264        (Some(b'\r'), Some(b'\n'), Some(b' ' | b'\t')) => 3,
265        _ => 0,
266    }
267}
268
269/// The text outside encoded words, checked as it is copied: UTF-8 (raw bytes
270/// above 0x7F are RFC 6532 text), with no control character but HTAB.
271struct Text {
272    utf8: Utf8,
273    prev: u8,
274}
275
276impl Text {
277    fn check(&mut self, b: u8, at: usize) -> Result<()> {
278        if b != b'\t' && crate::scan::is_control(b) {
279            return err(crate::scan::control_kind(b), at);
280        }
281        if self.prev == 0xc2 && (0x80..=0x9f).contains(&b) {
282            return err(ErrorKind::Control, at);
283        }
284        if !self.utf8.push(b) {
285            return err(ErrorKind::InvalidUtf8, at);
286        }
287        self.prev = b;
288        Ok(())
289    }
290
291    /// Copies a whitespace run, leaving out the CRLF of its folds.
292    fn run(&mut self, v: &[u8], s: usize, e: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
293        let mut i = s;
294        while i < e {
295            let n = ws_len(v, i);
296            let at = i + n - 1;
297            let wsp = v.get(at).copied().unwrap_or(b' ');
298            self.check(wsp, at)?;
299            emit(&[wsp]);
300            i += n;
301        }
302        Ok(())
303    }
304}
305
306/// The index of the `"` closing a quoted string whose body starts at `from`.
307fn closing_quote(v: &[u8], from: usize) -> Option<usize> {
308    let mut i = from;
309    loop {
310        match v.get(i)? {
311            b'\\' => i += 2,
312            b'"' => return Some(i),
313            _ => i += 1,
314        }
315    }
316}
317
318/// Decodes one Q word's text, emitting UTF-8.
319fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
320    let mut utf8 = Utf8::new();
321    let (mut i, mut prev) = (0usize, 0u8);
322    while let Some(&b) = text.get(i) {
323        let start = at + i;
324        let byte = match b {
325            b'_' => b' ',
326            b'=' => {
327                let hi = text.get(i + 1).copied().and_then(class::hex_value);
328                let lo = text.get(i + 2).copied().and_then(class::hex_value);
329                let (Some(hi), Some(lo)) = (hi, lo) else {
330                    return err(ErrorKind::InvalidEncodedWord, start);
331                };
332                i += 2;
333                (hi << 4) | lo
334            }
335            0x21..=0x7e => b,
336            _ => return err(ErrorKind::InvalidEncodedWord, start),
337        };
338        i += 1;
339        // Every control character is refused, as in ext-values: C0 (HTAB
340        // included), DEL, and C1 (U+0080 to U+009F: `C2 80` to `C2 9F` in
341        // UTF-8, where 0xC2 is always a lead byte; bytes 0x80 to 0x9F in
342        // ISO-8859-1).
343        let c1 = match cs {
344            Charset::Utf8 => prev == 0xc2 && (0x80..=0x9f).contains(&byte),
345            Charset::Latin1 => (0x80..=0x9f).contains(&byte),
346            Charset::Ascii => false,
347        };
348        if byte < 0x20 || byte == 0x7f || c1 {
349            return err(ErrorKind::ControlInDecoded, start);
350        }
351        prev = byte;
352        match cs {
353            Charset::Utf8 => {
354                if !utf8.push(byte) {
355                    return err(ErrorKind::InvalidUtf8, start);
356                }
357                emit(&[byte]);
358            }
359            Charset::Ascii if byte >= 0x80 => {
360                return err(ErrorKind::InvalidEncodedWord, start);
361            }
362            Charset::Ascii => emit(&[byte]),
363            Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
364            Charset::Latin1 => emit(&[byte]),
365        }
366    }
367    if !utf8.is_complete() {
368        // Each encoded word holds whole characters (RFC 2047 section 5).
369        return err(ErrorKind::InvalidUtf8, at + text.len());
370    }
371    Ok(())
372}
373
374/// Walks `v`, emitting the decoded text. Whitespace includes folds, whose
375/// CRLF is dropped, so the folded output of [`encode_words`] reads back. A
376/// quoted string starts at a word boundary and needs its closing `"`: a lone
377/// `"` (`5" disk`) is ordinary text. One pass: every scan ahead either
378/// consumes what it read or stops at the first byte that rules it out.
379fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
380    let mut text = Text {
381        utf8: Utf8::new(),
382        prev: 0,
383    };
384    let (mut i, mut words) = (0usize, 0usize);
385    let (mut boundary, mut after_word) = (true, false);
386    let mut held: Option<(usize, usize)> = None;
387    // Inside a quoted string while `i` is below this.
388    let mut quoted_until = 0usize;
389    // Cleared when no closing quote is left, so that scan is not repeated.
390    let mut closes = true;
391    while let Some(&b) = v.get(i) {
392        if i < quoted_until {
393            if ws_len(v, i) == 3 {
394                i += 2;
395                continue;
396            }
397            text.check(b, i)?;
398            emit(&[b]);
399            i += 1;
400            continue;
401        }
402        if ws_len(v, i) > 0 {
403            let start = i;
404            while ws_len(v, i) > 0 {
405                i += ws_len(v, i);
406            }
407            if after_word {
408                held = Some((start, i));
409            } else {
410                text.run(v, start, i, emit)?;
411            }
412            boundary = true;
413            continue;
414        }
415        if boundary && v.get(i..i + 2) == Some(b"=?") {
416            if let Some(Encoded {
417                charset: cs,
418                base64: b64,
419                text: word,
420                end,
421            }) = shape(v, i)?
422            {
423                if !text.utf8.is_complete() {
424                    return err(ErrorKind::InvalidUtf8, i);
425                }
426                words += 1;
427                if words > usize::from(limits.encoded_words) {
428                    return err(ErrorKind::TooManyMembers, i);
429                }
430                if b64 {
431                    // Base64 decoding comes with encode-lib (D-002).
432                    return err(ErrorKind::InvalidEncodedWord, i);
433                }
434                // Whitespace between two encoded words is dropped
435                // (RFC 2047 section 6.2).
436                held = None;
437                decode_q(cs, word, end - 2 - word.len(), emit)?;
438                after_word = true;
439                boundary = false;
440                i = end;
441                continue;
442            }
443        }
444        if let Some((s, e)) = held.take() {
445            text.run(v, s, e, emit)?;
446        }
447        after_word = false;
448        if b == b'"' && boundary && closes {
449            match closing_quote(v, i + 1) {
450                Some(close) => quoted_until = close + 1,
451                None => closes = false,
452            }
453        }
454        boundary = false;
455        text.check(b, i)?;
456        emit(&[b]);
457        i += 1;
458    }
459    if let Some((s, e)) = held {
460        text.run(v, s, e, emit)?;
461    }
462    if !text.utf8.is_complete() {
463        return err(ErrorKind::InvalidUtf8, v.len());
464    }
465    Ok(())
466}
467
468/// Decodes the RFC 2047 encoded words in unstructured header text into
469/// `out` and returns the text. Text outside encoded words is copied;
470/// whitespace between two adjacent encoded words is dropped (section 6.2);
471/// `_` is a space in Q; an encoded word must stand alone between whitespace,
472/// and words inside a quoted string are not decoded (section 5). Charsets:
473/// UTF-8, US-ASCII, ISO-8859-1 (transcoded), with an optional RFC 2231
474/// `*language`. `B` words need the `encode-lib` feature, which is not yet
475/// available (D-002). `CRLF WSP` folds count as whitespace, so the output of
476/// [`encode_words`] reads back as written.
477///
478/// # Errors
479///
480/// [`ErrorKind::UnsupportedCharset`]; [`ErrorKind::InvalidEncodedWord`] (a
481/// bad encoding letter, a broken `=XX`, a byte not allowed in a Q word, a
482/// `B` word); [`ErrorKind::ControlInDecoded`] for a decoded control
483/// character (C0 including CR, LF, NUL, and HTAB; DEL; C1), as in ext-values;
484/// a control kind for a control byte outside the words, other than HTAB and
485/// the CRLF of a fold; [`ErrorKind::InvalidUtf8`] (a UTF-8 word that is not
486/// whole characters, or text outside the words that is not UTF-8);
487/// [`ErrorKind::TooManyMembers`]
488/// beyond `max_encoded_words`; [`ErrorKind::BufferTooSmall`]. Nothing is
489/// written on error.
490///
491/// # Examples
492///
493/// ```
494/// use parse_net::{mail, Limits};
495///
496/// let mut buf = [0u8; 64];
497/// let s = mail::decode_words(b"=?utf-8?q?Caf=C3=A9?= =?ISO-8859-1?Q?_cr=E8me?= now", &Limits::DEFAULT, &mut buf)?;
498/// assert_eq!(s, "Caf\u{e9} cr\u{e8}me now");
499/// # Ok::<(), parse_net::ParseError>(())
500/// ```
501pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
502    let mut len = 0usize;
503    walk(value, limits, &mut |b: &[u8]| len += b.len())?;
504    let Some(dst) = out.get_mut(..len) else {
505        return err(ErrorKind::BufferTooSmall, 0);
506    };
507    let mut n = 0usize;
508    walk(value, limits, &mut |b: &[u8]| {
509        if let Some(d) = dst.get_mut(n..n + b.len()) {
510            d.copy_from_slice(b);
511        }
512        n += b.len();
513    })?;
514    // The first walk checked the text; this is the safe conversion.
515    core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
516}
517
518#[cfg(test)]
519mod tests {
520    use super::*;
521    use std::{string::String, vec};
522
523    fn enc(text: &str, room: usize) -> String {
524        let mut buf = vec![0u8; 2048];
525        let mut out = crate::SliceSink::new(&mut buf);
526        encode_words(text, room, &mut out).unwrap();
527        String::from_utf8(out.written().to_vec()).unwrap()
528    }
529
530    fn dec(v: &[u8]) -> Result<String> {
531        let mut buf = vec![0u8; 2048];
532        decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
533    }
534
535    #[test]
536    fn test_encode_words_edge_cases() {
537        assert_eq!(enc("plain ascii", 10), "plain ascii");
538        assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
539        let long = "\u{e9}".repeat(40);
540        let e = enc(&long, 75);
541        for word in e.split("\r\n ") {
542            assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
543            // Each word holds whole characters, so it decodes on its own.
544            assert!(dec(word.as_bytes()).is_ok());
545        }
546        assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
547        // An encoded word of exactly 75 bytes: 63 bytes of payload.
548        let e = enc(&"\u{20ac}".repeat(7), 75);
549        assert_eq!(e.len(), 12 + 63);
550        assert!(!e.starts_with("\r\n "));
551        assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
552        let mut buf = [0u8; 8];
553        let mut out = crate::SliceSink::new(&mut buf);
554        assert_eq!(
555            encode_words("a\nb", 70, &mut out),
556            Err(WriteError::Forbidden { at: 1 })
557        );
558        assert_eq!(
559            encode_words("a\u{85}", 70, &mut out),
560            Err(WriteError::Forbidden { at: 1 })
561        );
562        assert_eq!(
563            encode_words("\u{e9}\u{e9}", 70, &mut out),
564            Err(WriteError::Full)
565        );
566        assert!(out.is_empty());
567    }
568
569    #[test]
570    fn test_decode_words_edge_cases() {
571        assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
572        assert_eq!(dec(b"=?UTF-8?Q?a?=  \t =?UTF-8?Q?b?=").unwrap(), "ab");
573        assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
574        assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
575        assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
576        assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
577        assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
578        assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
579        assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
580        assert_eq!(
581            dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
582            ErrorKind::UnsupportedCharset
583        );
584        assert_eq!(
585            dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
586            ErrorKind::InvalidEncodedWord
587        );
588        assert_eq!(
589            dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
590            ErrorKind::InvalidEncodedWord
591        );
592        assert_eq!(
593            dec(b"=?UTF-8?B?YQ==?=").unwrap_err().kind(),
594            ErrorKind::InvalidEncodedWord
595        );
596        assert_eq!(
597            dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
598            ErrorKind::ControlInDecoded
599        );
600        assert_eq!(
601            dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
602            ErrorKind::InvalidUtf8
603        );
604        assert_eq!(
605            dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
606            ErrorKind::InvalidEncodedWord
607        );
608        assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
609        let l = Limits::DEFAULT.max_encoded_words(2);
610        let mut buf = [0u8; 64];
611        assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
612        assert_eq!(
613            decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
614                .unwrap_err()
615                .kind(),
616            ErrorKind::TooManyMembers
617        );
618        let mut small = [0u8; 2];
619        assert_eq!(
620            decode_words(b"abc", &Limits::DEFAULT, &mut small)
621                .unwrap_err()
622                .kind(),
623            ErrorKind::BufferTooSmall
624        );
625        assert_eq!(small, [0, 0]);
626    }
627}