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
288struct Encoded<'a> {
290 charset: Charset,
291 base64: bool,
293 text: &'a [u8],
294 end: usize,
296}
297
298fn 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
357fn 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
367struct 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 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
404fn 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#[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 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
457fn 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 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 return err(ErrorKind::InvalidUtf8, at + text.len());
493 }
494 Ok(())
495}
496
497fn 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 return err(ErrorKind::InvalidUtf8, at + text.len());
523 }
524 Ok(())
525}
526
527fn 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 let mut quoted_until = 0usize;
542 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 held = None;
586 let at = end - 2 - word.len();
587 if b64 {
588 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
627pub 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 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 assert!(dec(word.as_bytes()).is_ok());
707 }
708 assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
709 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 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 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 assert_eq!(dec(b"=?UTF-8?B?YW Jj?=").unwrap(), "=?UTF-8?B?YW Jj?=");
832 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 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 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 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 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 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 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 proptest::prop_assert_eq!(dec(value(&enc(&text, room)).as_bytes()).unwrap(), text);
917 }
918 }
919}