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
22fn q_len(c: char) -> usize {
24 let mut buf = [0u8; 4];
25 c.encode_utf8(&mut buf)
26 .bytes()
27 .map(|b| if q_plain(b) || b == b' ' { 1 } else { 3 })
28 .sum()
29}
30
31fn put_q(c: char, out: &mut impl Sink) -> core::result::Result<(), WriteError> {
32 const HEX: &[u8; 16] = b"0123456789ABCDEF";
33 let mut buf = [0u8; 4];
34 for b in c.encode_utf8(&mut buf).bytes() {
35 if b == b' ' {
36 out.put(b"_")?;
37 } else if q_plain(b) {
38 out.put(&[b])?;
39 } else {
40 let hi = HEX.get(usize::from(b >> 4)).copied().unwrap_or(b'0');
41 let lo = HEX.get(usize::from(b & 0xf)).copied().unwrap_or(b'0');
42 out.put(&[b'=', hi, lo])?;
43 }
44 }
45 Ok(())
46}
47
48fn put_words(
49 text: &str,
50 room_first_line: usize,
51 out: &mut impl Sink,
52) -> core::result::Result<(), WriteError> {
53 if text.bytes().all(|b| (0x20..=0x7e).contains(&b)) && !text.contains("=?") {
54 out.put(text.as_bytes())?;
55 return Ok(());
56 }
57 let mut limit = room_first_line.min(WORD_LIMIT);
58 let mut payload = 0usize;
59 let mut open = false;
60 for c in text.chars() {
61 let n = q_len(c);
62 if open && OVERHEAD + payload + n > limit {
63 out.put(b"?=\r\n ")?;
64 open = false;
65 limit = WORD_LIMIT;
66 }
67 if !open {
68 if OVERHEAD + n > limit {
69 out.put(b"\r\n ")?;
72 limit = WORD_LIMIT;
73 }
74 out.put(b"=?UTF-8?Q?")?;
75 open = true;
76 payload = 0;
77 }
78 put_q(c, out)?;
79 payload += n;
80 }
81 if open {
82 out.put(b"?=")?;
83 }
84 Ok(())
85}
86
87pub fn encode_words(
110 text: &str,
111 room_first_line: usize,
112 out: &mut impl Sink,
113) -> core::result::Result<(), WriteError> {
114 if let Some((at, _)) = text.char_indices().find(|&(_, c)| c.is_control()) {
115 return Err(WriteError::forbidden(at));
116 }
117 let mut count = CountSink::new();
118 put_words(text, room_first_line, &mut count)?;
119 reserve(out, count.count())?;
120 put_words(text, room_first_line, out)
121}
122
123#[derive(Clone, Copy, PartialEq, Eq)]
125enum Charset {
126 Utf8,
127 Ascii,
128 Latin1,
129}
130
131struct Encoded<'a> {
133 charset: Charset,
134 base64: bool,
136 text: &'a [u8],
137 end: usize,
139}
140
141fn shape(v: &[u8], i: usize) -> Result<Option<Encoded<'_>>> {
144 let rest = v.get(i + 2..).unwrap_or(&[]);
145 let Some(q1) = rest.iter().position(|&b| b == b'?') else {
146 return Ok(None);
147 };
148 let charset = rest.get(..q1).unwrap_or(&[]);
149 if charset.is_empty()
150 || !charset
151 .iter()
152 .all(|&b| class::is_mime_token_char(b) && b != b'?')
153 {
154 return Ok(None);
155 }
156 let after = rest.get(q1 + 1..).unwrap_or(&[]);
157 let (Some(&enc), Some(b'?')) = (after.first(), after.get(1)) else {
158 return Ok(None);
159 };
160 let text_start = i + 2 + q1 + 3;
161 let text_rest = v.get(text_start..).unwrap_or(&[]);
162 let Some(end) = text_rest.windows(2).position(|w| w == b"?=") else {
163 return Ok(None);
164 };
165 let text = text_rest.get(..end).unwrap_or(&[]);
166 if text.iter().any(|&b| b == b' ' || b == b'\t' || b == b'?') {
167 return Ok(None);
168 }
169 let name = charset.split(|&b| b == b'*').next().unwrap_or(&[]);
171 let cs = if name.eq_ignore_ascii_case(b"utf-8") {
172 Charset::Utf8
173 } else if name.eq_ignore_ascii_case(b"us-ascii") {
174 Charset::Ascii
175 } else if name.eq_ignore_ascii_case(b"iso-8859-1") {
176 Charset::Latin1
177 } else {
178 return err(ErrorKind::UnsupportedCharset, i + 2);
179 };
180 let b = match enc {
181 b'Q' | b'q' => false,
182 b'B' | b'b' => true,
183 _ => return err(ErrorKind::InvalidEncodedWord, i + 2 + q1 + 1),
184 };
185 Ok(Some(Encoded {
186 charset: cs,
187 base64: b,
188 text,
189 end: text_start + end + 2,
190 }))
191}
192
193fn decode_q(cs: Charset, text: &[u8], at: usize, emit: &mut impl FnMut(&[u8])) -> Result<()> {
195 let mut utf8 = Utf8::new();
196 let mut i = 0usize;
197 while let Some(&b) = text.get(i) {
198 let byte = match b {
199 b'_' => b' ',
200 b'=' => {
201 let hi = text.get(i + 1).copied().and_then(class::hex_value);
202 let lo = text.get(i + 2).copied().and_then(class::hex_value);
203 let (Some(hi), Some(lo)) = (hi, lo) else {
204 return err(ErrorKind::InvalidEncodedWord, at + i);
205 };
206 i += 2;
207 (hi << 4) | lo
208 }
209 0x21..=0x7e => b,
210 _ => return err(ErrorKind::InvalidEncodedWord, at + i),
211 };
212 i += 1;
213 if matches!(byte, b'\r' | b'\n' | 0) {
214 return err(ErrorKind::ControlInDecoded, at + i - 1);
215 }
216 match cs {
217 Charset::Utf8 => {
218 if !utf8.push(byte) {
219 return err(ErrorKind::InvalidUtf8, at + i - 1);
220 }
221 emit(&[byte]);
222 }
223 Charset::Ascii if byte >= 0x80 => {
224 return err(ErrorKind::InvalidEncodedWord, at + i - 1);
225 }
226 Charset::Ascii => emit(&[byte]),
227 Charset::Latin1 if byte >= 0x80 => emit(&[0xc0 | (byte >> 6), 0x80 | (byte & 0x3f)]),
228 Charset::Latin1 => emit(&[byte]),
229 }
230 }
231 if !utf8.is_complete() {
232 return err(ErrorKind::InvalidUtf8, at + text.len());
234 }
235 Ok(())
236}
237
238fn walk(v: &[u8], limits: &Limits, emit: &mut impl FnMut(&[u8])) -> Result<()> {
240 let (mut i, mut words) = (0usize, 0usize);
241 let (mut boundary, mut quoted, mut after_word) = (true, false, false);
242 let mut held: Option<(usize, usize)> = None;
243 while let Some(&b) = v.get(i) {
244 if quoted {
245 emit(&[b]);
246 if b == b'\\' {
247 if let Some(&n) = v.get(i + 1) {
248 emit(&[n]);
249 i += 1;
250 }
251 } else if b == b'"' {
252 quoted = false;
253 }
254 i += 1;
255 continue;
256 }
257 if b == b' ' || b == b'\t' {
258 let start = i;
259 while matches!(v.get(i), Some(b' ' | b'\t')) {
260 i += 1;
261 }
262 if after_word {
263 held = Some((start, i));
264 } else {
265 emit(v.get(start..i).unwrap_or(&[]));
266 }
267 boundary = true;
268 continue;
269 }
270 if boundary && v.get(i..i + 2) == Some(b"=?") {
271 if let Some(Encoded {
272 charset: cs,
273 base64: b64,
274 text,
275 end,
276 }) = shape(v, i)?
277 {
278 if end == v.len() || matches!(v.get(end), Some(b' ' | b'\t')) {
279 words += 1;
280 if words > usize::from(limits.encoded_words) {
281 return err(ErrorKind::TooManyMembers, i);
282 }
283 if b64 {
284 return err(ErrorKind::InvalidEncodedWord, i);
286 }
287 held = None;
290 let text_at = end - 2 - text.len();
291 decode_q(cs, text, text_at, emit)?;
292 after_word = true;
293 boundary = false;
294 i = end;
295 continue;
296 }
297 }
298 }
299 if let Some((s, e)) = held.take() {
300 emit(v.get(s..e).unwrap_or(&[]));
301 }
302 after_word = false;
303 boundary = false;
304 if b == b'"' {
305 quoted = true;
306 }
307 emit(&[b]);
308 i += 1;
309 }
310 if let Some((s, e)) = held {
311 emit(v.get(s..e).unwrap_or(&[]));
312 }
313 Ok(())
314}
315
316pub fn decode_words<'b>(value: &[u8], limits: &Limits, out: &'b mut [u8]) -> Result<&'b str> {
346 let mut len = 0usize;
347 walk(value, limits, &mut |b: &[u8]| len += b.len())?;
348 let Some(dst) = out.get_mut(..len) else {
349 return err(ErrorKind::BufferTooSmall, 0);
350 };
351 let mut n = 0usize;
352 walk(value, limits, &mut |b: &[u8]| {
353 if let Some(d) = dst.get_mut(n..n + b.len()) {
354 d.copy_from_slice(b);
355 }
356 n += b.len();
357 })?;
358 if let Err(e) = core::str::from_utf8(dst) {
359 let at = e.valid_up_to();
360 dst.fill(0);
362 return Err(ParseError::new(ErrorKind::InvalidUtf8, at));
363 }
364 core::str::from_utf8(dst).map_err(|e| ParseError::new(ErrorKind::InvalidUtf8, e.valid_up_to()))
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370 use std::{string::String, vec};
371
372 fn enc(text: &str, room: usize) -> String {
373 let mut buf = vec![0u8; 2048];
374 let mut out = crate::SliceSink::new(&mut buf);
375 encode_words(text, room, &mut out).unwrap();
376 String::from_utf8(out.written().to_vec()).unwrap()
377 }
378
379 fn dec(v: &[u8]) -> Result<String> {
380 let mut buf = vec![0u8; 2048];
381 decode_words(v, &Limits::DEFAULT, &mut buf).map(String::from)
382 }
383
384 #[test]
385 fn test_encode_words_edge_cases() {
386 assert_eq!(enc("plain ascii", 10), "plain ascii");
387 assert_eq!(enc("a =?b", 70), "=?UTF-8?Q?a_=3D=3Fb?=");
388 let long = "\u{e9}".repeat(40);
389 let e = enc(&long, 75);
390 for word in e.split("\r\n ") {
391 assert!(word.len() <= 75 && word.starts_with("=?UTF-8?Q?") && word.ends_with("?="));
392 assert!(dec(word.as_bytes()).is_ok());
394 }
395 assert_eq!(dec(e.replace("\r\n", "").as_bytes()).unwrap(), long);
396 let e = enc(&"\u{20ac}".repeat(7), 75);
398 assert_eq!(e.len(), 12 + 63);
399 assert!(!e.starts_with("\r\n "));
400 assert!(enc("\u{e9}", 5).starts_with("\r\n =?UTF-8?Q?"));
401 let mut buf = [0u8; 8];
402 let mut out = crate::SliceSink::new(&mut buf);
403 assert_eq!(
404 encode_words("a\nb", 70, &mut out),
405 Err(WriteError::Forbidden { at: 1 })
406 );
407 assert_eq!(
408 encode_words("a\u{85}", 70, &mut out),
409 Err(WriteError::Forbidden { at: 1 })
410 );
411 assert_eq!(
412 encode_words("\u{e9}\u{e9}", 70, &mut out),
413 Err(WriteError::Full)
414 );
415 assert!(out.is_empty());
416 }
417
418 #[test]
419 fn test_decode_words_edge_cases() {
420 assert_eq!(dec(b"=?utf-8?q?a_b?=").unwrap(), "a b");
421 assert_eq!(dec(b"=?UTF-8?Q?a?= \t =?UTF-8?Q?b?=").unwrap(), "ab");
422 assert_eq!(dec(b"=?UTF-8?Q?a?= x =?UTF-8?Q?b?= ").unwrap(), "a x b ");
423 assert_eq!(dec(b"\"=?UTF-8?Q?a?=\"").unwrap(), "\"=?UTF-8?Q?a?=\"");
424 assert_eq!(dec(b"x=?UTF-8?Q?a?=").unwrap(), "x=?UTF-8?Q?a?=");
425 assert_eq!(dec(b"=?UTF-8?Q?a?=x").unwrap(), "=?UTF-8?Q?a?=x");
426 assert_eq!(dec(b"=?UTF-8*en?Q?a?=").unwrap(), "a");
427 assert_eq!(dec(b"=? not a word").unwrap(), "=? not a word");
428 assert_eq!(dec(b"=?US-ASCII?Q?ok?=").unwrap(), "ok");
429 assert_eq!(
430 dec(b"=?KOI8-R?Q?a?=").unwrap_err().kind(),
431 ErrorKind::UnsupportedCharset
432 );
433 assert_eq!(
434 dec(b"=?UTF-8?X?a?=").unwrap_err().kind(),
435 ErrorKind::InvalidEncodedWord
436 );
437 assert_eq!(
438 dec(b"=?UTF-8?Q?=4?=").unwrap_err().kind(),
439 ErrorKind::InvalidEncodedWord
440 );
441 assert_eq!(
442 dec(b"=?UTF-8?B?YQ==?=").unwrap_err().kind(),
443 ErrorKind::InvalidEncodedWord
444 );
445 assert_eq!(
446 dec(b"=?UTF-8?Q?=0D=0A?=").unwrap_err().kind(),
447 ErrorKind::ControlInDecoded
448 );
449 assert_eq!(
450 dec(b"=?UTF-8?Q?=C3?=").unwrap_err().kind(),
451 ErrorKind::InvalidUtf8
452 );
453 assert_eq!(
454 dec(b"=?US-ASCII?Q?=E9?=").unwrap_err().kind(),
455 ErrorKind::InvalidEncodedWord
456 );
457 assert_eq!(dec(b"raw \xe9").unwrap_err().kind(), ErrorKind::InvalidUtf8);
458 let l = Limits::DEFAULT.max_encoded_words(2);
459 let mut buf = [0u8; 64];
460 assert!(decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?=", &l, &mut buf).is_ok());
461 assert_eq!(
462 decode_words(b"=?UTF-8?Q?a?= =?UTF-8?Q?b?= =?UTF-8?Q?c?=", &l, &mut buf)
463 .unwrap_err()
464 .kind(),
465 ErrorKind::TooManyMembers
466 );
467 let mut small = [0u8; 2];
468 assert_eq!(
469 decode_words(b"abc", &Limits::DEFAULT, &mut small)
470 .unwrap_err()
471 .kind(),
472 ErrorKind::BufferTooSmall
473 );
474 assert_eq!(small, [0, 0]);
475 }
476}