1use crate::{
4 class,
5 error::{ErrorKind, ParseError, Result, WriteError, err},
6 limits::Limits,
7 utf8::Utf8,
8 write::{CountSink, Sink, reserve},
9};
10
11const WORD_LIMIT: usize = 75;
13const OVERHEAD: usize = 12;
15
16const fn q_plain(b: u8) -> bool {
19 b.is_ascii_alphanumeric() || matches!(b, b'!' | b'*' | b'+' | b'-' | b'/')
20}
21
22#[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#[inline]
40fn q_byte_len(b: u8) -> usize {
41 usize::from(Q_LEN.get(usize::from(b)).copied().unwrap_or(3))
42}
43
44const 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
54struct Payload {
57 buf: [u8; WORD_LIMIT - OVERHEAD],
58 len: usize,
59}
60
61impl Payload {
62 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 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 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
145pub 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#[derive(Clone, Copy, PartialEq, Eq)]
184enum Charset {
185 Utf8,
186 Ascii,
187 Latin1,
188}
189
190struct Encoded<'a> {
192 charset: Charset,
193 base64: bool,
195 text: &'a [u8],
196 end: usize,
198}
199
200fn 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 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
259fn 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
269struct 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 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
306fn 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
318fn 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 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 return err(ErrorKind::InvalidUtf8, at + text.len());
370 }
371 Ok(())
372}
373
374fn 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 let mut quoted_until = 0usize;
389 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 return err(ErrorKind::InvalidEncodedWord, i);
433 }
434 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
468pub 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 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 assert!(dec(word.as_bytes()).is_ok());
545 }
546 assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
547 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}