use super::b64;
use crate::{
class,
error::{ErrorKind, ParseError, Result, WriteError, err},
limits::Limits,
utf8::Utf8,
write::{CountSink, Sink, reserve},
};
const WORD_LIMIT: usize = 75;
const OVERHEAD: usize = 12;
const fn q_plain(b: u8) -> bool {
b.is_ascii_alphanumeric() || matches!(b, b'!' | b'*' | b'+' | b'-' | b'/')
}
#[allow(clippy::indexing_slicing)]
const Q_LEN: [u8; 256] = {
let mut t = [3u8; 256];
let mut b = 0usize;
while b < 256 {
if q_plain(b as u8) || b == b' ' as usize {
t[b] = 1;
}
b += 1;
}
t
};
#[inline]
fn q_byte_len(b: u8) -> usize {
usize::from(Q_LEN.get(usize::from(b)).copied().unwrap_or(3))
}
const fn utf8_width(b: u8) -> usize {
match b {
0x00..=0x7f => 1,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
_ => 4,
}
}
struct Payload {
buf: [u8; WORD_LIMIT - OVERHEAD],
len: usize,
}
impl Payload {
fn push(&mut self, ch: &[u8]) {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
for &b in ch {
let len = self.len;
if q_byte_len(b) == 1 {
if let Some(d) = self.buf.get_mut(len) {
*d = if b == b' ' { b'_' } else { b };
}
self.len = len + 1;
} else {
let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
if let Some([x, y, z]) = self.buf.get_mut(len..len + 3) {
(*x, *y, *z) = (b'=', hi, lo);
}
self.len = len + 3;
}
}
}
fn as_bytes(&self) -> &[u8] {
self.buf.get(..self.len).unwrap_or(&[])
}
}
#[inline]
fn goes_plain(text: &str, room_first_line: usize) -> bool {
let bytes = text.as_bytes();
let used = super::SOFT_LIMIT.saturating_sub(room_first_line);
bytes.iter().all(|b| (0x20..=0x7e).contains(b))
&& !text.contains("=?")
&& used + bytes.len() <= super::HARD_LIMIT
}
fn put_words(
text: &str,
room_first_line: usize,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
let bytes = text.as_bytes();
let used = super::SOFT_LIMIT.saturating_sub(room_first_line);
if bytes.iter().all(|b| (0x20..=0x7e).contains(b))
&& !text.contains("=?")
&& used + bytes.len() <= super::HARD_LIMIT
{
out.put(bytes)?;
return Ok(());
}
let mut limit = room_first_line.min(WORD_LIMIT);
let mut word = Payload {
buf: [0; WORD_LIMIT - OVERHEAD],
len: 0,
};
let mut open = false;
let mut i = 0usize;
while let Some(&lead) = bytes.get(i) {
let ch = bytes.get(i..i + utf8_width(lead)).unwrap_or(&[]);
i += ch.len().max(1);
let mut n = 0usize;
for &b in ch {
n += q_byte_len(b);
}
if open && OVERHEAD + word.len + n > limit {
out.put(word.as_bytes())?;
out.put(b"?=\r\n ")?;
open = false;
limit = WORD_LIMIT;
}
if !open {
if OVERHEAD + n > limit {
out.put(b"\r\n ")?;
limit = WORD_LIMIT;
}
out.put(b"=?UTF-8?Q?")?;
open = true;
word.len = 0;
}
word.push(ch);
}
if open {
out.put(word.as_bytes())?;
out.put(b"?=")?;
}
Ok(())
}
pub fn encode_words(
text: &str,
room_first_line: usize,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
return Err(WriteError::forbidden(at));
}
let mut count = CountSink::new();
put_words(text, room_first_line, &mut count)?;
reserve(out, count.count())?;
put_words(text, room_first_line, out)
}
fn put_b_word(chunk: &[u8], out: &mut impl Sink) -> core::result::Result<(), WriteError> {
let mut buf = [0u8; WORD_LIMIT - OVERHEAD];
let n = b64::encode(chunk, &mut buf);
out.put(b"=?UTF-8?B?")?;
out.put(buf.get(..n).unwrap_or(&[]))?;
out.put(b"?=")?;
Ok(())
}
fn put_words_b(
text: &str,
room_first_line: usize,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
let bytes = text.as_bytes();
if goes_plain(text, room_first_line) {
out.put(bytes)?;
return Ok(());
}
let mut limit = room_first_line.min(WORD_LIMIT);
let (mut start, mut len) = (0usize, 0usize);
while let Some(&lead) = bytes.get(start + len) {
let w = utf8_width(lead);
if len > 0 && OVERHEAD + b64::encoded_len(len + w) > limit {
put_b_word(bytes.get(start..start + len).unwrap_or(&[]), out)?;
out.put(b"\r\n ")?;
(start, len, limit) = (start + len, 0, WORD_LIMIT);
}
if len == 0 && OVERHEAD + b64::encoded_len(w) > limit {
out.put(b"\r\n ")?;
limit = WORD_LIMIT;
}
len += w;
}
if len > 0 {
put_b_word(bytes.get(start..start + len).unwrap_or(&[]), out)?;
}
Ok(())
}
pub fn encode_words_b(
text: &str,
room_first_line: usize,
out: &mut impl Sink,
) -> core::result::Result<(), WriteError> {
if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
return Err(WriteError::forbidden(at));
}
let mut count = CountSink::new();
put_words_b(text, room_first_line, &mut count)?;
reserve(out, count.count())?;
put_words_b(text, room_first_line, out)
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum Charset {
Utf8,
Ascii,
Latin1,
}
struct Encoded<'a> {
charset: Charset,
base64: bool,
text: &'a [u8],
end: usize,
}
fn shape(v: &[u8], i: usize) -> Result<Option<Encoded<'_>>> {
let rest = v.get(i + 2..).unwrap_or(&[]);
let q1 = rest
.iter()
.position(|&b| !class::is_mime_token_char(b))
.unwrap_or(rest.len());
if q1 == 0 || rest.get(q1) != Some(&b'?') {
return Ok(None);
}
let charset = rest.get(..q1).unwrap_or(&[]);
let after = rest.get(q1 + 1..).unwrap_or(&[]);
let (Some(&enc), Some(b'?')) = (after.first(), after.get(1)) else {
return Ok(None);
};
let text_start = i + 2 + q1 + 3;
let text_rest = v.get(text_start..).unwrap_or(&[]);
let Some(end) = text_rest
.iter()
.position(|&b| matches!(b, b'?' | b' ' | b'\t'))
else {
return Ok(None);
};
if text_rest.get(end..end + 2) != Some(b"?=") {
return Ok(None);
}
let text = text_rest.get(..end).unwrap_or(&[]);
let after_word = text_start + end + 2;
if after_word != v.len() && ws_len(v, after_word) == 0 {
return Ok(None);
}
let name = charset.split(|&b| b == b'*').next().unwrap_or(&[]);
let cs = if name.eq_ignore_ascii_case(b"utf-8") {
Charset::Utf8
} else if name.eq_ignore_ascii_case(b"us-ascii") {
Charset::Ascii
} else if name.eq_ignore_ascii_case(b"iso-8859-1") {
Charset::Latin1
} else {
return err(ErrorKind::UnsupportedCharset, i + 2);
};
let b = match enc {
b'Q' | b'q' => false,
b'B' | b'b' => true,
_ => return err(ErrorKind::InvalidEncodedWord, i + 2 + q1 + 1),
};
Ok(Some(Encoded {
charset: cs,
base64: b,
text,
end: after_word,
}))
}
fn ws_len(v: &[u8], i: usize) -> usize {
match (v.get(i), v.get(i + 1), v.get(i + 2)) {
(Some(b' ' | b'\t'), _, _) => 1,
(Some(b'\r'), Some(b'\n'), Some(b' ' | b'\t')) => 3,
_ => 0,
}
}
struct Text {
utf8: Utf8,
prev: u8,
}
impl Text {
fn check(&mut self, b: u8, at: usize) -> Result<()> {
if b != b'\t' && crate::scan::is_control(b) {
return err(crate::scan::control_kind(b), at);
}
if self.prev == 0xc2 && (0x80..=0x9f).contains(&b) {
return err(ErrorKind::Control, at);
}
if !self.utf8.push(b) {
return err(ErrorKind::InvalidUtf8, at);
}
self.prev = b;
Ok(())
}
fn run(&mut self, v: &[u8], s: usize, e: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
let mut i = s;
while i < e {
let n = ws_len(v, i);
let at = i + n - 1;
let wsp = v.get(at).copied().unwrap_or(b' ');
self.check(wsp, at)?;
emit(&[wsp]);
i += n;
}
Ok(())
}
}
fn closing_quote(v: &[u8], from: usize) -> Option<usize> {
let mut i = from;
loop {
match v.get(i)? {
b'\\' => i += 2,
b'"' => return Some(i),
_ => i += 1,
}
}
}
#[inline(always)]
fn put_decoded(
cs: Charset,
byte: u8,
prev: &mut u8,
utf8: &mut Utf8,
start: usize,
emit: &mut impl FnMut(&[u8]),
) -> Result<()> {
let c1 = match cs {
Charset::Utf8 => *prev == 0xc2 && (0x80..=0x9f).contains(&byte),
Charset::Latin1 => (0x80..=0x9f).contains(&byte),
Charset::Ascii => false,
};
if byte < 0x20 || byte == 0x7f || c1 {
return err(ErrorKind::ControlInDecoded, start);
}
*prev = byte;
match cs {
Charset::Utf8 => {
if !utf8.push(byte) {
return err(ErrorKind::InvalidUtf8, start);
}
emit(&[byte]);
}
Charset::Ascii if byte >= 0x80 => {
return err(ErrorKind::InvalidEncodedWord, start);
}
Charset::Ascii => emit(&[byte]),
Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
Charset::Latin1 => emit(&[byte]),
}
Ok(())
}
fn decode_b(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
let mut bytes = [0u8; b64::MAX_DECODED];
let n =
b64::decode(text, &mut bytes).or_else(|i| err(ErrorKind::InvalidEncodedWord, at + i))?;
let decoded = bytes.get(..n).unwrap_or(&[]);
let ascii_text = |d: &[u8]| d.iter().all(|&b| (0x20..0x7f).contains(&b));
let clean = match cs {
Charset::Utf8 => {
decoded.iter().all(|&b| b >= 0x20 && b != 0x7f)
&& !decoded
.iter()
.zip(decoded.iter().skip(1))
.any(|(&a, &b)| a == 0xc2 && (0x80..=0x9f).contains(&b))
&& core::str::from_utf8(decoded).is_ok()
}
Charset::Ascii | Charset::Latin1 => ascii_text(decoded),
};
if clean {
emit(decoded);
return Ok(());
}
let mut utf8 = Utf8::new();
let mut prev = 0u8;
for (k, &byte) in decoded.iter().enumerate() {
put_decoded(cs, byte, &mut prev, &mut utf8, at + k / 3 * 4, emit)?;
}
if !utf8.is_complete() {
return err(ErrorKind::InvalidUtf8, at + text.len());
}
Ok(())
}
fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
let mut utf8 = Utf8::new();
let (mut i, mut prev) = (0usize, 0u8);
while let Some(&b) = text.get(i) {
let start = at + i;
let byte = match b {
b'_' => b' ',
b'=' => {
let hi = text.get(i + 1).copied().and_then(class::hex_value);
let lo = text.get(i + 2).copied().and_then(class::hex_value);
let (Some(hi), Some(lo)) = (hi, lo) else {
return err(ErrorKind::InvalidEncodedWord, start);
};
i += 2;
(hi << 4) | lo
}
0x21..=0x7e => b,
_ => return err(ErrorKind::InvalidEncodedWord, start),
};
i += 1;
put_decoded(cs, byte, &mut prev, &mut utf8, start, emit)?;
}
if !utf8.is_complete() {
return err(ErrorKind::InvalidUtf8, at + text.len());
}
Ok(())
}
fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
let mut text = Text {
utf8: Utf8::new(),
prev: 0,
};
let (mut i, mut words) = (0usize, 0usize);
let (mut boundary, mut after_word) = (true, false);
let mut held: Option<(usize, usize)> = None;
let mut quoted_until = 0usize;
let mut closes = true;
while let Some(&b) = v.get(i) {
if i < quoted_until {
if ws_len(v, i) == 3 {
i += 2;
continue;
}
text.check(b, i)?;
emit(&[b]);
i += 1;
continue;
}
if ws_len(v, i) > 0 {
let start = i;
while ws_len(v, i) > 0 {
i += ws_len(v, i);
}
if after_word {
held = Some((start, i));
} else {
text.run(v, start, i, emit)?;
}
boundary = true;
continue;
}
if boundary && v.get(i..i + 2) == Some(b"=?") {
if let Some(Encoded {
charset: cs,
base64: b64,
text: word,
end,
}) = shape(v, i)?
{
if !text.utf8.is_complete() {
return err(ErrorKind::InvalidUtf8, i);
}
words += 1;
if words > usize::from(limits.encoded_words) {
return err(ErrorKind::TooManyMembers, i);
}
held = None;
let at = end - 2 - word.len();
if b64 {
if end - i > WORD_LIMIT {
return err(ErrorKind::InvalidEncodedWord, i);
}
decode_b(cs, word, at, emit)?;
} else {
decode_q(cs, word, at, emit)?;
}
after_word = true;
boundary = false;
i = end;
continue;
}
}
if let Some((s, e)) = held.take() {
text.run(v, s, e, emit)?;
}
after_word = false;
if b == b'"' && boundary && closes {
match closing_quote(v, i + 1) {
Some(close) => quoted_until = close + 1,
None => closes = false,
}
}
boundary = false;
text.check(b, i)?;
emit(&[b]);
i += 1;
}
if let Some((s, e)) = held {
text.run(v, s, e, emit)?;
}
if !text.utf8.is_complete() {
return err(ErrorKind::InvalidUtf8, v.len());
}
Ok(())
}
pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
let mut len = 0usize;
walk(value, limits, &mut |b: &[u8]| len += b.len())?;
let Some(dst) = out.get_mut(..len) else {
return err(ErrorKind::BufferTooSmall, 0);
};
let mut n = 0usize;
walk(value, limits, &mut |b: &[u8]| {
if let Some(d) = dst.get_mut(n..n + b.len()) {
d.copy_from_slice(b);
}
n += b.len();
})?;
core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
}
#[cfg(test)]
mod tests {
use super::*;
use std::{format, string::String, vec, vec::Vec};
fn enc(text: &str, room: usize) -> String {
let mut buf = vec![0u8; 2048];
let mut out = crate::SliceSink::new(&mut buf);
encode_words(text, room, &mut out).unwrap();
String::from_utf8(out.written().to_vec()).unwrap()
}
fn dec(v: &[u8]) -> Result<String> {
let mut buf = vec![0u8; 2048];
decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
}
#[test]
fn test_encode_words_edge_cases() {
assert_eq!(enc("plain ascii", 10), "plain ascii");
assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
let long = "\u{e9}".repeat(40);
let e = enc(&long, 75);
for word in e.split("\r\n ") {
assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
assert!(dec(word.as_bytes()).is_ok());
}
assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
let e = enc(&"\u{20ac}".repeat(7), 75);
assert_eq!(e.len(), 12 + 63);
assert!(!e.starts_with("\r\n "));
assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
let mut buf = [0u8; 8];
let mut out = crate::SliceSink::new(&mut buf);
assert_eq!(
encode_words("a\nb", 70, &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert_eq!(
encode_words("a\u{85}", 70, &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert_eq!(
encode_words("\u{e9}\u{e9}", 70, &mut out),
Err(WriteError::Full)
);
assert!(out.is_empty());
}
#[test]
fn test_decode_words_edge_cases() {
assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
assert_eq!(dec(b"=?UTF-8?Q?a?= \t =?UTF-8?Q?b?=").unwrap(), "ab");
assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
assert_eq!(
dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
ErrorKind::UnsupportedCharset
);
assert_eq!(
dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
ErrorKind::InvalidEncodedWord
);
assert_eq!(
dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
ErrorKind::InvalidEncodedWord
);
assert_eq!(dec(b"=?UTF-8?B?YQ==?=").unwrap(), "a");
assert_eq!(
dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
ErrorKind::ControlInDecoded
);
assert_eq!(
dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
ErrorKind::InvalidUtf8
);
assert_eq!(
dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
ErrorKind::InvalidEncodedWord
);
assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
let l = Limits::DEFAULT.max_encoded_words(2);
let mut buf = [0u8; 64];
assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
assert_eq!(
decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
.unwrap_err()
.kind(),
ErrorKind::TooManyMembers
);
let mut small = [0u8; 2];
assert_eq!(
decode_words(b"abc", &Limits::DEFAULT, &mut small)
.unwrap_err()
.kind(),
ErrorKind::BufferTooSmall
);
assert_eq!(small, [0, 0]);
}
fn enc_b(text: &str, room: usize) -> String {
let mut buf = vec![0u8; 4096];
let mut out = crate::SliceSink::new(&mut buf);
encode_words_b(text, room, &mut out).unwrap();
String::from_utf8(out.written().to_vec()).unwrap()
}
#[test]
fn test_decode_b_words() {
assert_eq!(
dec(b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=").unwrap(),
"If you can read this yo"
);
assert_eq!(
dec(b"=?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=\r\n =?ISO-8859-1?Q?u?=")
.unwrap(),
"If you can read this you"
);
assert_eq!(
dec(b"=?utf-8?b?Q2Fmw6k=?= au lait").unwrap(),
"Caf\u{e9} au lait"
);
assert_eq!(dec(b"=?ISO-8859-1?B?6Q==?=").unwrap(), "\u{e9}");
assert_eq!(dec(b"=?UTF-8*ja?B?5Lya?=").unwrap(), "\u{4f1a}");
assert_eq!(dec(b"=?UTF-8?B??=").unwrap(), "");
let at = |v: &[u8]| {
let e = dec(v).unwrap_err();
(e.kind(), e.offset())
};
assert_eq!(at(b"=?UTF-8?B?YQ=?="), (ErrorKind::InvalidEncodedWord, 10));
assert_eq!(at(b"=?UTF-8?B?YR==?="), (ErrorKind::InvalidEncodedWord, 11));
assert_eq!(
at(b"=?UTF-8?B?YQ==YQ==?="),
(ErrorKind::InvalidEncodedWord, 12)
);
assert_eq!(at(b"=?UTF-8?B?Y-Q=?="), (ErrorKind::InvalidEncodedWord, 11));
assert_eq!(
at(b"=?UTF-8?B?YWJj\x80?="),
(ErrorKind::InvalidEncodedWord, 14)
);
assert_eq!(dec(b"=?UTF-8?B?YW Jj?=").unwrap(), "=?UTF-8?B?YW Jj?=");
assert_eq!(at(b"=?UTF-8?B?YQ0K?="), (ErrorKind::ControlInDecoded, 10));
assert_eq!(
at(b"=?UTF-8?B?YWJjGw==?="),
(ErrorKind::ControlInDecoded, 14)
);
assert_eq!(at(b"=?UTF-8?B?woU=?="), (ErrorKind::ControlInDecoded, 10));
assert_eq!(at(b"=?ISO-8859-1?B?hQ==?=").0, ErrorKind::ControlInDecoded);
assert_eq!(dec(b"=?UTF-8?B?w6k=?=x").unwrap(), "=?UTF-8?B?w6k=?=x");
assert_eq!(at(b"=?UTF-8?B?ww==?=").0, ErrorKind::InvalidUtf8);
assert_eq!(at(b"=?US-ASCII?B?6Q==?=").0, ErrorKind::InvalidEncodedWord);
let word = |n: usize| format!("=?UTF-8?B?{}?=", "QUFB".repeat(n));
assert_eq!(dec(word(15).as_bytes()).unwrap(), "A".repeat(45));
let long = format!("=?UTF-8?B?{}QQ==?=", "QUFB".repeat(15));
assert_eq!(long.len(), 76);
assert_eq!(at(long.as_bytes()), (ErrorKind::InvalidEncodedWord, 0));
assert_eq!(at(word(16).as_bytes()), (ErrorKind::InvalidEncodedWord, 0));
}
#[test]
fn test_encode_words_b_edge_cases() {
assert_eq!(enc_b("plain ascii", 10), "plain ascii");
assert_eq!(enc_b("a =?b", 70), "=?UTF-8?B?YSA9P2I=?=");
assert_eq!(enc_b("Caf\u{e9}", 70), "=?UTF-8?B?Q2Fmw6k=?=");
let text = "\u{4f1a}\u{8b70}".repeat(30);
let e = enc_b(&text, 75);
for w in e.split("\r\n ") {
assert!(
w.len() <= 75 && w.starts_with("=?UTF-8?B?") && w.ends_with("?="),
"{w}"
);
assert!(dec(w.as_bytes()).is_ok());
}
assert_eq!(dec(e.as_bytes()).unwrap(), text);
let e = enc_b(&"\u{20ac}".repeat(15), 75);
assert_eq!(e.len(), 72);
let e = enc_b(&"\u{20ac}".repeat(16), 75);
assert_eq!(e.split("\r\n ").map(str::len).collect::<Vec<_>>(), [72, 16]);
assert!(enc_b("\u{e9}", 16).starts_with("=?UTF-8?B?"));
assert!(enc_b("\u{e9}", 15).starts_with("\r\n =?UTF-8?B?"));
assert!(
enc_b(&"\u{e9}".repeat(20), 20)
.split("\r\n ")
.next()
.unwrap()
.len()
<= 20
);
let subject = "\u{4f1a}\u{8b70}\u{306e}\u{304a}\u{77e5}\u{3089}\u{305b}";
assert!(enc_b(subject, 64).len() * 10 < enc(subject, 64).len() * 7);
let mut buf = [0u8; 8];
let mut out = crate::SliceSink::new(&mut buf);
assert_eq!(
encode_words_b("a\u{1b}", 70, &mut out),
Err(WriteError::Forbidden { at: 1 })
);
assert_eq!(
encode_words_b("\u{e9}\u{e9}", 70, &mut out),
Err(WriteError::Full)
);
assert!(out.is_empty());
}
proptest::proptest! {
#[test]
fn test_encode_words_b_round_trips(text in "[^\\p{Cc}]{0,120}", room in 0usize..80) {
let e = enc_b(&text, room);
for (i, w) in e.split("\r\n ").enumerate() {
if w.starts_with("=?") {
proptest::prop_assert!(w.len() <= if i == 0 { room.min(75) } else { 75 }, "{w}");
}
}
let value = |s: &str| String::from(s.strip_prefix("\r\n ").unwrap_or(s));
proptest::prop_assert_eq!(&dec(value(&e).as_bytes()).unwrap(), &text);
proptest::prop_assert_eq!(dec(value(&enc(&text, room)).as_bytes()).unwrap(), text);
}
}
}