1use 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
12const WORD_LIMIT: usize = 75;
14const OVERHEAD: usize = 12;
16
17const fn q_plain(b: u8) -> bool {
20 b.is_ascii_alphanumeric() || matches!(b, b'!' | b'*' | b'+' | b'-' | b'/')
21}
22
23#[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#[inline]
41fn q_byte_len(b: u8) -> usize {
42 usize::from(Q_LEN.get(usize::from(b)).copied().unwrap_or(3))
43}
44
45const 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
55struct Payload {
58 buf: [u8; WORD_LIMIT - OVERHEAD],
59 len: usize,
60}
61
62impl Payload {
63 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#[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 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 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
158pub 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
195fn 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 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 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
239pub 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#[derive(Clone, Copy, PartialEq, Eq)]
282enum Charset {
283 Utf8,
284 Ascii,
285 Latin1,
286}
287
288pub(super) struct Encoded<'a> {
290 charset: Charset,
291 base64: bool,
293 text: &'a [u8],
294 pub(super) end: usize,
296}
297
298pub(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 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
357pub(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
367pub(super) struct Text {
370 utf8: Utf8,
371 prev: u8,
372}
373
374impl Text {
375 pub(super) const fn new() -> Self {
377 Self {
378 utf8: Utf8::new(),
379 prev: 0,
380 }
381 }
382
383 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 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
417fn 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#[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 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
470fn 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 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 return err(ErrorKind::InvalidUtf8, at + text.len());
506 }
507 Ok(())
508}
509
510fn 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 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 return err(ErrorKind::InvalidUtf8, at + text.len());
551 }
552 Ok(())
553}
554
555pub(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 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
585fn 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 let mut quoted_until = 0usize;
597 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 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
672pub 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 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 assert!(dec(word.as_bytes()).is_ok());
752 }
753 assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
754 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 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 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 assert_eq!(dec(b"=?UTF-8?B?YW Jj?=").unwrap(), "=?UTF-8?B?YW Jj?=");
877 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 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 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 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 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 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 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 proptest::prop_assert_eq!(dec(value(&enc(&text, room)).as_bytes()).unwrap(), text);
962 }
963 }
964}