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 if bytes.iter().all(|b| (0x20..=0x7e).contains(b)) && !text.contains("=?") {
96 out.put(bytes)?;
97 return Ok(());
98 }
99 let mut limit = room_first_line.min(WORD_LIMIT);
100 let mut word = Payload {
101 buf: [0; WORD_LIMIT - OVERHEAD],
102 len: 0,
103 };
104 let mut open = false;
105 let mut i = 0usize;
106 while let Some(&lead) = bytes.get(i) {
107 let ch = bytes.get(i..i + utf8_width(lead)).unwrap_or(&[]);
108 i += ch.len().max(1);
109 let mut n = 0usize;
110 for &b in ch {
111 n += q_byte_len(b);
112 }
113 if open && OVERHEAD + word.len + n > limit {
114 out.put(word.as_bytes())?;
115 out.put(b"?=\r\n ")?;
116 open = false;
117 limit = WORD_LIMIT;
118 }
119 if !open {
120 if OVERHEAD + n > limit {
121 out.put(b"\r\n ")?;
124 limit = WORD_LIMIT;
125 }
126 out.put(b"=?UTF-8?Q?")?;
127 open = true;
128 word.len = 0;
129 }
130 word.push(ch);
131 }
132 if open {
133 out.put(word.as_bytes())?;
134 out.put(b"?=")?;
135 }
136 Ok(())
137}
138
139pub fn encode_words(
162 text: &str,
163 room_first_line: usize,
164 out: &mut impl Sink,
165) -> core::result::Result<(), WriteError> {
166 if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
167 return Err(WriteError::forbidden(at));
168 }
169 let mut count = CountSink::new();
170 put_words(text, room_first_line, &mut count)?;
171 reserve(out, count.count())?;
172 put_words(text, room_first_line, out)
173}
174
175#[derive(Clone, Copy, PartialEq, Eq)]
177enum Charset {
178 Utf8,
179 Ascii,
180 Latin1,
181}
182
183struct Encoded<'a> {
185 charset: Charset,
186 base64: bool,
188 text: &'a [u8],
189 end: usize,
191}
192
193fn shape(v: &[u8], i: usize) -> Result<Option<Encoded<'_>>> {
196 let rest = v.get(i + 2..).unwrap_or(&[]);
197 let Some(q1) = rest.iter().position(|&b| b == b'?') else {
198 return Ok(None);
199 };
200 let charset = rest.get(..q1).unwrap_or(&[]);
201 if charset.is_empty()
202 || !charset
203 .iter()
204 .all(|&b| class::is_mime_token_char(b) && b != b'?')
205 {
206 return Ok(None);
207 }
208 let after = rest.get(q1 + 1..).unwrap_or(&[]);
209 let (Some(&enc), Some(b'?')) = (after.first(), after.get(1)) else {
210 return Ok(None);
211 };
212 let text_start = i + 2 + q1 + 3;
213 let text_rest = v.get(text_start..).unwrap_or(&[]);
214 let Some(end) = text_rest.windows(2).position(|w| w == b"?=") else {
215 return Ok(None);
216 };
217 let text = text_rest.get(..end).unwrap_or(&[]);
218 if text.iter().any(|&b| b == b' ' || b == b'\t' || b == b'?') {
219 return Ok(None);
220 }
221 let name = charset.split(|&b| b == b'*').next().unwrap_or(&[]);
223 let cs = if name.eq_ignore_ascii_case(b"utf-8") {
224 Charset::Utf8
225 } else if name.eq_ignore_ascii_case(b"us-ascii") {
226 Charset::Ascii
227 } else if name.eq_ignore_ascii_case(b"iso-8859-1") {
228 Charset::Latin1
229 } else {
230 return err(ErrorKind::UnsupportedCharset, i + 2);
231 };
232 let b = match enc {
233 b'Q' | b'q' => false,
234 b'B' | b'b' => true,
235 _ => return err(ErrorKind::InvalidEncodedWord, i + 2 + q1 + 1),
236 };
237 Ok(Some(Encoded {
238 charset: cs,
239 base64: b,
240 text,
241 end: text_start + end + 2,
242 }))
243}
244
245fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
247 let mut utf8 = Utf8::new();
248 let mut i = 0usize;
249 while let Some(&b) = text.get(i) {
250 let byte = match b {
251 b'_' => b' ',
252 b'=' => {
253 let hi = text.get(i + 1).copied().and_then(class::hex_value);
254 let lo = text.get(i + 2).copied().and_then(class::hex_value);
255 let (Some(hi), Some(lo)) = (hi, lo) else {
256 return err(ErrorKind::InvalidEncodedWord, at + i);
257 };
258 i += 2;
259 (hi << 4) | lo
260 }
261 0x21..=0x7e => b,
262 _ => return err(ErrorKind::InvalidEncodedWord, at + i),
263 };
264 i += 1;
265 if matches!(byte, b'\r' | b'\n' | 0) {
266 return err(ErrorKind::ControlInDecoded, at + i - 1);
267 }
268 match cs {
269 Charset::Utf8 => {
270 if !utf8.push(byte) {
271 return err(ErrorKind::InvalidUtf8, at + i - 1);
272 }
273 emit(&[byte]);
274 }
275 Charset::Ascii if byte >= 0x80 => {
276 return err(ErrorKind::InvalidEncodedWord, at + i - 1);
277 }
278 Charset::Ascii => emit(&[byte]),
279 Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
280 Charset::Latin1 => emit(&[byte]),
281 }
282 }
283 if !utf8.is_complete() {
284 return err(ErrorKind::InvalidUtf8, at + text.len());
286 }
287 Ok(())
288}
289
290fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
292 let (mut i, mut words) = (0usize, 0usize);
293 let (mut boundary, mut quoted, mut after_word) = (true, false, false);
294 let mut held: Option<(usize, usize)> = None;
295 while let Some(&b) = v.get(i) {
296 if quoted {
297 emit(&[b]);
298 if b == b'\\' {
299 if let Some(&n) = v.get(i + 1) {
300 emit(&[n]);
301 i += 1;
302 }
303 } else if b == b'"' {
304 quoted = false;
305 }
306 i += 1;
307 continue;
308 }
309 if b == b' ' || b == b'\t' {
310 let start = i;
311 while matches!(v.get(i), Some(b' ' | b'\t')) {
312 i += 1;
313 }
314 if after_word {
315 held = Some((start, i));
316 } else {
317 emit(v.get(start..i).unwrap_or(&[]));
318 }
319 boundary = true;
320 continue;
321 }
322 if boundary && v.get(i..i + 2) == Some(b"=?") {
323 if let Some(Encoded {
324 charset: cs,
325 base64: b64,
326 text,
327 end,
328 }) = shape(v, i)?
329 {
330 if end == v.len() || matches!(v.get(end), Some(b' ' | b'\t')) {
331 words += 1;
332 if words > usize::from(limits.encoded_words) {
333 return err(ErrorKind::TooManyMembers, i);
334 }
335 if b64 {
336 return err(ErrorKind::InvalidEncodedWord, i);
338 }
339 held = None;
342 let text_at = end - 2 - text.len();
343 decode_q(cs, text, text_at, emit)?;
344 after_word = true;
345 boundary = false;
346 i = end;
347 continue;
348 }
349 }
350 }
351 if let Some((s, e)) = held.take() {
352 emit(v.get(s..e).unwrap_or(&[]));
353 }
354 after_word = false;
355 boundary = false;
356 if b == b'"' {
357 quoted = true;
358 }
359 emit(&[b]);
360 i += 1;
361 }
362 if let Some((s, e)) = held {
363 emit(v.get(s..e).unwrap_or(&[]));
364 }
365 Ok(())
366}
367
368pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
398 let mut len = 0usize;
399 walk(value, limits, &mut |b: &[u8]| len += b.len())?;
400 let Some(dst) = out.get_mut(..len) else {
401 return err(ErrorKind::BufferTooSmall, 0);
402 };
403 let mut n = 0usize;
404 walk(value, limits, &mut |b: &[u8]| {
405 if let Some(d) = dst.get_mut(n..n + b.len()) {
406 d.copy_from_slice(b);
407 }
408 n += b.len();
409 })?;
410 if let Err(e) = core::str::from_utf8(dst) {
411 let at = e.valid_up_to();
412 dst.fill(0);
414 return Err(ParseError::new(ErrorKind::InvalidUtf8, at));
415 }
416 core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
417}
418
419#[cfg(test)]
420mod tests {
421 use super::*;
422 use std::{string::String, vec};
423
424 fn enc(text: &str, room: usize) -> String {
425 let mut buf = vec![0u8; 2048];
426 let mut out = crate::SliceSink::new(&mut buf);
427 encode_words(text, room, &mut out).unwrap();
428 String::from_utf8(out.written().to_vec()).unwrap()
429 }
430
431 fn dec(v: &[u8]) -> Result<String> {
432 let mut buf = vec![0u8; 2048];
433 decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
434 }
435
436 #[test]
437 fn test_encode_words_edge_cases() {
438 assert_eq!(enc("plain ascii", 10), "plain ascii");
439 assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
440 let long = "\u{e9}".repeat(40);
441 let e = enc(&long, 75);
442 for word in e.split("\r\n ") {
443 assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
444 assert!(dec(word.as_bytes()).is_ok());
446 }
447 assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
448 let e = enc(&"\u{20ac}".repeat(7), 75);
450 assert_eq!(e.len(), 12 + 63);
451 assert!(!e.starts_with("\r\n "));
452 assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
453 let mut buf = [0u8; 8];
454 let mut out = crate::SliceSink::new(&mut buf);
455 assert_eq!(
456 encode_words("a\nb", 70, &mut out),
457 Err(WriteError::Forbidden { at: 1 })
458 );
459 assert_eq!(
460 encode_words("a\u{85}", 70, &mut out),
461 Err(WriteError::Forbidden { at: 1 })
462 );
463 assert_eq!(
464 encode_words("\u{e9}\u{e9}", 70, &mut out),
465 Err(WriteError::Full)
466 );
467 assert!(out.is_empty());
468 }
469
470 #[test]
471 fn test_decode_words_edge_cases() {
472 assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
473 assert_eq!(dec(b"=?UTF-8?Q?a?= \t =?UTF-8?Q?b?=").unwrap(), "ab");
474 assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
475 assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
476 assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
477 assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
478 assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
479 assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
480 assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
481 assert_eq!(
482 dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
483 ErrorKind::UnsupportedCharset
484 );
485 assert_eq!(
486 dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
487 ErrorKind::InvalidEncodedWord
488 );
489 assert_eq!(
490 dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
491 ErrorKind::InvalidEncodedWord
492 );
493 assert_eq!(
494 dec(b"=?UTF-8?B?YQ==?=").unwrap_err().kind(),
495 ErrorKind::InvalidEncodedWord
496 );
497 assert_eq!(
498 dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
499 ErrorKind::ControlInDecoded
500 );
501 assert_eq!(
502 dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
503 ErrorKind::InvalidUtf8
504 );
505 assert_eq!(
506 dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
507 ErrorKind::InvalidEncodedWord
508 );
509 assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
510 let l = Limits::DEFAULT.max_encoded_words(2);
511 let mut buf = [0u8; 64];
512 assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
513 assert_eq!(
514 decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
515 .unwrap_err()
516 .kind(),
517 ErrorKind::TooManyMembers
518 );
519 let mut small = [0u8; 2];
520 assert_eq!(
521 decode_words(b"abc", &Limits::DEFAULT, &mut small)
522 .unwrap_err()
523 .kind(),
524 ErrorKind::BufferTooSmall
525 );
526 assert_eq!(small, [0, 0]);
527 }
528}