1#[cfg(feature = "arbitrary")]
2use arbitrary::Arbitrary;
3use bounded_static::ToStatic;
4use nom::{
5 branch::alt,
6 bytes::complete::{tag, take_while1},
7 combinator::{map, opt},
8 multi::{many0, many1, separated_list0},
9 sequence::delimited,
10 IResult, Parser,
11};
12use std::borrow::Cow;
13#[cfg(feature = "tracing")]
14use tracing::warn;
15
16use crate::i18n::ContainsUtf8;
17use crate::print::{print_seq, Formatter, Print, ToStringFromPrint};
18use crate::text::{
19 ascii,
20 encoding::{self, encoded_word, encoded_word_plain},
21 quoted::{quoted_string, QuotedString, QuotedStringChars},
22 utf8::take_utf8_while1,
23 whitespace::{cfws, fws, is_obs_no_ws_ctl},
24 words::{atom, is_vchar, mime_atom, Atom, MIMEAtom, MIMEAtomChars},
25};
26#[cfg(feature = "arbitrary")]
27use crate::{
28 arbitrary_utils::{
29 arbitrary_string_nonempty_where, arbitrary_vec_nonempty, arbitrary_whitespace_nonempty,
30 },
31 fuzz_eq::FuzzEq,
32};
33use eml_codec_derives::instrument_input;
34
35#[derive(Clone, ContainsUtf8, Debug, PartialEq, Default, ToStatic, ToStringFromPrint)]
36#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
37pub struct PhraseList<'a>(pub Vec<Phrase<'a>>); #[cfg(feature = "arbitrary")]
40impl<'a> Arbitrary<'a> for PhraseList<'a> {
41 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
42 Ok(Self(arbitrary_vec_nonempty(u)?))
43 }
44}
45
46#[instrument_input("tracing")]
57pub fn phrase_list(input: &[u8]) -> IResult<&[u8], Option<PhraseList<'_>>> {
58 let (input, phrases_opt) =
59 separated_list0(tag(","), alt((map(phrase, Some), map(opt(cfws), |_| None))))(input)?;
60 let phrases: Vec<Phrase> = phrases_opt.into_iter().flatten().collect();
61 if phrases.is_empty() {
62 Ok((input, None))
63 } else {
64 Ok((input, Some(PhraseList(phrases))))
65 }
66}
67impl<'a> Print for PhraseList<'a> {
68 fn print(&self, fmt: &mut impl Formatter) {
69 print_seq(fmt, &self.0, |fmt| {
70 fmt.write_bytes(b",");
71 fmt.write_fws()
72 })
73 }
74}
75
76#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic, ToStringFromPrint)]
77#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
78pub enum MIMEWord<'a> {
79 Quoted(QuotedString<'a>),
80 Atom(MIMEAtom<'a>),
81}
82impl Default for MIMEWord<'static> {
83 fn default() -> Self {
84 Self::Atom(MIMEAtom::default())
85 }
86}
87#[instrument_input("tracing")]
88pub fn mime_word(input: &[u8]) -> IResult<&[u8], MIMEWord<'_>> {
89 alt((
90 map(quoted_string, MIMEWord::Quoted),
91 map(mime_atom, MIMEWord::Atom),
92 ))(input)
93}
94
95impl<'a> MIMEWord<'a> {
96 pub fn chars<'b>(&'b self) -> MIMEWordChars<'a, 'b> {
97 match self {
98 MIMEWord::Quoted(q) => MIMEWordChars::Quoted(q.chars()),
99 MIMEWord::Atom(a) => MIMEWordChars::Atom(a.chars()),
100 }
101 }
102}
103impl<'a> Print for MIMEWord<'a> {
104 fn print(&self, fmt: &mut impl Formatter) {
105 match self {
106 MIMEWord::Quoted(q) => q.print(fmt),
107 MIMEWord::Atom(a) => a.print(fmt),
108 }
109 }
110}
111
112#[derive(Clone)]
113pub enum MIMEWordChars<'a, 'b> {
114 Quoted(QuotedStringChars<'a, 'b>),
115 Atom(MIMEAtomChars<'a, 'b>),
116}
117
118impl<'a, 'b> Iterator for MIMEWordChars<'a, 'b> {
119 type Item = char;
120 fn next(&mut self) -> Option<Self::Item> {
121 match self {
122 MIMEWordChars::Quoted(q) => q.next(),
123 MIMEWordChars::Atom(a) => a.next(),
124 }
125 }
126}
127
128#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic, ToStringFromPrint)]
129#[cfg_attr(feature = "arbitrary", derive(Arbitrary, FuzzEq))]
130pub enum Word<'a> {
131 Quoted(QuotedString<'a>),
132 Atom(Atom<'a>),
133}
134
135impl<'a> Print for Word<'a> {
136 fn print(&self, fmt: &mut impl Formatter) {
137 match self {
138 Word::Quoted(q) => q.print(fmt),
139 Word::Atom(a) => a.print(fmt),
140 }
141 }
142}
143
144impl<'a> Word<'a> {
145 pub fn chars<'b>(&'b self) -> WordChars<'a, 'b> {
146 match self {
147 Word::Quoted(q) => WordChars::Quoted(q.chars()),
148 Word::Atom(a) => WordChars::Atom(a.0.chars()),
149 }
150 }
151}
152
153impl<'a> TryFrom<&'a str> for Word<'a> {
154 type Error = (); fn try_from(s: &'a str) -> Result<Self, Self::Error> {
156 if let Ok(a) = Atom::try_from(s) {
157 return Ok(Word::Atom(a));
158 }
159 let qs = QuotedString::try_from(s)?;
160 Ok(Word::Quoted(qs))
161 }
162}
163
164#[derive(Clone)]
165pub enum WordChars<'a, 'b> {
166 Quoted(QuotedStringChars<'a, 'b>),
167 Atom(std::str::Chars<'b>),
168}
169
170impl<'a, 'b> Iterator for WordChars<'a, 'b> {
171 type Item = char;
172 fn next(&mut self) -> Option<Self::Item> {
173 match self {
174 WordChars::Quoted(q) => q.next(),
175 WordChars::Atom(a) => a.next(),
176 }
177 }
178}
179
180#[instrument_input("tracing")]
186pub fn word(input: &[u8]) -> IResult<&[u8], Word<'_>> {
187 alt((map(quoted_string, Word::Quoted), map(atom, Word::Atom)))(input)
188}
189
190#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic, ToStringFromPrint)]
191#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
192pub enum PhraseToken<'a> {
193 Word(Word<'a>),
196 Encoded(encoding::EncodedWord<'a>),
197}
198impl<'a> Print for PhraseToken<'a> {
199 fn print(&self, fmt: &mut impl Formatter) {
200 match self {
201 PhraseToken::Word(w) => w.print(fmt),
202 PhraseToken::Encoded(e) => e.print(fmt),
203 }
204 }
205}
206impl<'a> PhraseToken<'a> {
207 pub fn data(&self) -> String {
209 match self {
210 Self::Word(w) => w.chars().collect(),
211 Self::Encoded(e) => e.data(),
212 }
213 }
214}
215#[cfg(feature = "arbitrary")]
216impl<'a> Arbitrary<'a> for PhraseToken<'a> {
217 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
218 if u.arbitrary()? {
219 let w: Word<'_> = u.arbitrary()?;
220 if let Word::Atom(a) = &w {
223 if a.0.find("=?").is_some() {
224 return Err(arbitrary::Error::IncorrectFormat);
225 }
226 }
227 Ok(PhraseToken::Word(w))
228 } else {
229 Ok(PhraseToken::Encoded(u.arbitrary()?))
230 }
231 }
232}
233
234#[instrument_input("tracing")]
236pub fn phrase_token(input: &[u8]) -> IResult<&[u8], PhraseToken<'_>> {
237 alt((
238 map(
240 encoded_word(encoding::Context::Phrase),
241 PhraseToken::Encoded,
242 ),
243 map(word, PhraseToken::Word),
244 map(
253 delimited(opt(cfws), tag(&[ascii::PERIOD][..]), opt(cfws)),
254 |_| {
255 PhraseToken::Word(Word::Quoted(QuotedString(vec![Cow::Owned(
256 ".".to_string(),
257 )])))
258 },
259 ),
260 ))(input)
261}
262
263#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic, ToStringFromPrint)]
265pub struct Phrase<'a>(pub Vec<PhraseToken<'a>>);
266
267impl<'a> Print for Phrase<'a> {
268 fn print(&self, fmt: &mut impl Formatter) {
269 print_seq(fmt, &self.0, Formatter::write_fws)
270 }
271}
272
273impl<'a, 'b> From<&'b [&'a str]> for Phrase<'a> {
274 fn from(mut words: &'b [&'a str]) -> Self {
283 let mut toks = vec![];
284 let mut to_encode: Vec<&str> = vec![];
285
286 let push_encoded = |to_encode: &mut Vec<&str>, toks: &mut Vec<_>| {
287 if !to_encode.is_empty() {
288 toks.push(PhraseToken::Encoded(encoding::EncodedWord::from_chars(
289 to_encode.join(" ").chars(),
290 )));
291 to_encode.clear();
292 }
293 };
294
295 while let [w, ws @ ..] = words {
296 if w.is_ascii() {
301 if let Ok(w) = Word::try_from(*w) {
302 push_encoded(&mut to_encode, &mut toks);
303 toks.push(PhraseToken::Word(w));
304 words = ws;
305 continue;
306 }
307 }
308 to_encode.push(w);
309 words = ws;
310 }
311 push_encoded(&mut to_encode, &mut toks);
312 Self(toks)
313 }
314}
315impl From<&[String]> for Phrase<'static> {
316 fn from(words: &[String]) -> Self {
317 use bounded_static::ToBoundedStatic;
318 let words: Vec<&str> = words.as_ref().iter().map(|s| s.as_str()).collect();
319 Phrase::from(words.as_slice()).to_static()
320 }
321}
322impl<'a> From<&'a str> for Phrase<'a> {
323 fn from(word: &'a str) -> Self {
324 Phrase::from(&[word][..])
325 }
326}
327impl From<String> for Phrase<'static> {
328 fn from(word: String) -> Self {
329 use bounded_static::ToBoundedStatic;
330 Phrase::from(&[word][..]).to_static()
331 }
332}
333
334impl<'a> Phrase<'a> {
335 pub fn data(&self) -> Vec<String> {
337 self.0.iter().map(|tok| tok.data()).collect()
338 }
339
340 #[cfg(feature = "arbitrary")]
342 fn normalize(&self) -> Self {
343 let mut v = Vec::new();
344 for tok in &self.0 {
345 match (v.last_mut(), tok) {
346 (Some(PhraseToken::Encoded(ref mut e1)), PhraseToken::Encoded(e2)) => {
347 e1.0.extend(e2.0.clone())
348 }
349 (_, tok) => v.push(tok.clone()),
350 }
351 }
352 Self(v)
353 }
354}
355#[cfg(feature = "arbitrary")]
356impl<'a> Arbitrary<'a> for Phrase<'a> {
357 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
358 Ok(Self(arbitrary_vec_nonempty(u)?))
359 }
360}
361#[cfg(feature = "arbitrary")]
362impl<'a> FuzzEq for Phrase<'a> {
363 fn fuzz_eq(&self, other: &Self) -> bool {
364 self.normalize().0.fuzz_eq(&other.normalize().0)
365 }
366}
367
368#[instrument_input("tracing")]
385pub fn phrase(input: &[u8]) -> IResult<&[u8], Phrase<'_>> {
386 let (input, phrase) = map(many1(phrase_token), Phrase)(input)?;
387 Ok((input, phrase))
388}
389
390#[derive(Debug, PartialEq, Clone, ToStatic)]
391pub struct UtextToken<'a> {
392 txt: Cow<'a, str>,
393 obs: bool,
394}
395
396fn obs_utext_token<'a>(input: &'a [u8]) -> IResult<&'a [u8], UtextToken<'a>> {
409 alt((
410 take_utf8_while1(is_vchar).map(|s| UtextToken { txt: s, obs: false }),
411 take_while1(|c| is_obs_no_ws_ctl(c) || c == ascii::NULL)
412 .map(|s| unsafe { str::from_utf8_unchecked(s) })
415 .map(|s| UtextToken {
416 txt: Cow::Borrowed(s),
417 obs: true,
418 }),
419 ))(input)
420}
421
422#[derive(Debug, PartialEq, Copy, Clone, ToStatic)]
423pub enum UnstrTxtKind {
424 Txt, Obs, Fws, }
428
429#[derive(PartialEq, Clone, Debug, ToStatic)]
430#[cfg_attr(feature = "arbitrary", derive(FuzzEq))]
431pub enum UnstrToken<'a> {
432 Encoded(encoding::EncodedWord<'a>),
433 #[cfg_attr(feature = "arbitrary", fuzz_eq(use_eq))]
436 Plain(Cow<'a, str>, UnstrTxtKind),
437}
438
439impl<'a> UnstrToken<'a> {
440 pub(crate) fn from_plain(s: &'a str, kind: UnstrTxtKind) -> Self {
441 Self::Plain(Cow::Borrowed(s), kind)
442 }
443
444 fn from_utext(tok: UtextToken<'a>) -> Self {
445 if tok.obs {
446 Self::Plain(tok.txt, UnstrTxtKind::Obs)
447 } else {
448 Self::Plain(tok.txt, UnstrTxtKind::Txt)
449 }
450 }
451}
452impl<'a> ContainsUtf8 for UnstrToken<'a> {
453 fn contains_utf8(&self) -> bool {
454 match self {
455 UnstrToken::Encoded(_) => false,
456 UnstrToken::Plain(s, _) => s.contains_utf8(),
457 }
458 }
459}
460impl<'a> Print for UnstrToken<'a> {
461 fn print(&self, fmt: &mut impl Formatter) {
462 match self {
463 UnstrToken::Encoded(e) => e.print(fmt),
464 UnstrToken::Plain(txt, UnstrTxtKind::Txt) => fmt.write_bytes(txt.as_bytes()),
465 UnstrToken::Plain(_, UnstrTxtKind::Obs) =>
466 {}
468 UnstrToken::Plain(txt, UnstrTxtKind::Fws) => fmt.write_fws_bytes(txt.as_bytes()),
469 }
470 }
471}
472#[cfg(feature = "arbitrary")]
473impl<'a> Arbitrary<'a> for UnstrToken<'a> {
474 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
475 match u.int_in_range(0..=2)? {
478 0 => Ok(UnstrToken::Encoded(u.arbitrary()?)),
479 1 => {
480 let txt = arbitrary_string_nonempty_where(u, is_vchar, 'X')?;
481 if txt.find("=?").is_some() {
484 return Err(arbitrary::Error::IncorrectFormat);
485 }
486 Ok(UnstrToken::Plain(txt.into(), UnstrTxtKind::Txt))
487 }
488 2 => {
489 let txt = arbitrary_whitespace_nonempty(u)?;
490 Ok(UnstrToken::Plain(txt.into(), UnstrTxtKind::Fws))
491 }
492 _ => unreachable!(),
493 }
494 }
495}
496
497#[derive(Debug, PartialEq, Clone, ToStatic, ToStringFromPrint)]
503pub struct Unstructured<'a>(pub Vec<UnstrToken<'a>>);
504
505impl<'a> Print for Unstructured<'a> {
506 fn print(&self, fmt: &mut impl Formatter) {
507 for i in 0..self.0.len() {
508 let tok = &self.0[i];
509
510 if i > 0 {
512 if let (UnstrToken::Encoded(_), UnstrToken::Encoded(_)) = (&self.0[i - 1], tok) {
513 fmt.write_fws()
514 }
515 }
516
517 tok.print(fmt)
518 }
519 }
520}
521
522impl<'a> Unstructured<'a> {
523 pub fn to_string_keep_obs(&self) -> String {
524 let mut s = String::new();
525 for tok in &self.0 {
526 match tok {
527 UnstrToken::Encoded(e) => s.push_str(&e.to_string()),
528 UnstrToken::Plain(txt, _) => s.push_str(txt),
529 }
530 }
531 s
532 }
533
534 #[cfg(feature = "arbitrary")]
537 fn fuzz_eq_normalize(&self) -> Unstructured<'static> {
538 use bounded_static::ToBoundedStatic;
539 let mut v: Vec<UnstrToken<'static>> = Vec::new();
540 for tok in &self.0 {
541 match (v.last_mut(), tok) {
542 (Some(UnstrToken::Plain(s1, k1)), UnstrToken::Plain(s2, k2)) if k1 == k2 => {
543 s1.to_mut().push_str(s2)
544 }
545 (Some(UnstrToken::Encoded(e1)), UnstrToken::Encoded(e2)) => {
546 e1.0.extend(e2.to_static().0)
547 }
548 _ => v.push(tok.to_static()),
549 }
550 }
551 Unstructured(v)
552 }
553}
554impl<'a> ContainsUtf8 for Unstructured<'a> {
555 fn contains_utf8(&self) -> bool {
556 self.0.contains_utf8()
557 }
558}
559
560#[cfg(feature = "arbitrary")]
561impl<'a> FuzzEq for Unstructured<'a> {
562 fn fuzz_eq(&self, other: &Self) -> bool {
563 self.fuzz_eq_normalize()
564 .0
565 .fuzz_eq(&other.fuzz_eq_normalize().0)
566 }
567}
568
569#[cfg(feature = "arbitrary")]
570impl<'a> Arbitrary<'a> for Unstructured<'a> {
571 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
572 enum Kind {
573 Encoded,
574 Wsp,
575 Txt,
576 }
577 fn k(tok: &UnstrToken<'_>) -> Kind {
578 match tok {
579 UnstrToken::Encoded(_) => Kind::Encoded,
580 UnstrToken::Plain(_, UnstrTxtKind::Fws) => Kind::Wsp,
581 UnstrToken::Plain(_, _) => Kind::Txt,
582 }
583 }
584
585 let mut v: Vec<UnstrToken> = Vec::new();
586 let mut before_last = None;
587 let mut last = None;
588 for _ in 0..u.arbitrary_len::<UnstrToken>()? {
589 let tok: UnstrToken = u.arbitrary()?;
590 match (&before_last, &last, k(&tok)) {
591 (Some(Kind::Encoded), Some(Kind::Wsp), Kind::Encoded) |
593 (_, Some(Kind::Encoded), Kind::Txt) | (_, Some(Kind::Txt), Kind::Encoded) => {
595 return Err(arbitrary::Error::IncorrectFormat)
596 },
597
598 (_, Some(Kind::Wsp), Kind::Wsp) | (_, Some(Kind::Txt), Kind::Txt) =>
601 (),
602 (_, _, ktok) => {
603 before_last = last;
604 last = Some(ktok);
605 }
606 };
607 v.push(tok)
608 }
609 Ok(Unstructured(v))
610 }
611}
612
613#[instrument_input("tracing")]
633pub fn unstructured(input: &[u8]) -> IResult<&[u8], Unstructured<'_>> {
634 let (input, r) = many0(alt((
635 map(encoded_word_plain(encoding::Context::Unstructured), |w| {
636 vec![UnstrToken::Encoded(w)]
637 }),
638 map(obs_utext_token, |tok| vec![UnstrToken::from_utext(tok)]),
639 map(fws, |v| {
640 v.into_iter()
641 .map(|s| UnstrToken::from_plain(s, UnstrTxtKind::Fws))
642 .collect()
643 }),
644 )))(input)?;
645
646 Ok((input, Unstructured(r.into_iter().flatten().collect())))
647}
648
649#[cfg(test)]
650mod tests {
651 use super::*;
652 use crate::print::tests::print_to_vec;
653 use crate::text::charset::EmailCharset;
654 use crate::text::encoding::{EncodedWord, EncodedWordToken, QuotedChunk, QuotedWord};
655
656 #[test]
657 fn test_word_from() {
658 assert_eq!(Word::try_from("abc"), Ok(Word::Atom(Atom("abc".into()))),);
659
660 assert_eq!(Word::try_from("").unwrap().to_string(), "\"\"",);
661
662 assert_eq!(Word::try_from("a b").unwrap().to_string(), "\"a b\"",);
663 }
664
665 #[test]
666 fn test_phrase_from() {
667 let check = |words: &[&str]| {
668 let p = Phrase::from(words);
669 assert_eq!(words.join(" "), p.data().join(" "))
670 };
671
672 check(&["abc", "d ef", "éé", "à"]);
673 check(&["abc", "", "", "à"]);
674 check(&["éè", "a", "ö", " ï", "w"]);
675
676 assert_eq!(
677 Phrase::from(&["a", "", "b c", "é", "ï"][..]).to_string(),
678 "a \"\" \"b c\" =?UTF-8?Q?=C3=A9_=C3=AF?=",
679 );
680 }
681
682 #[test]
683 fn test_phrase() {
684 assert_eq!(
685 print_to_vec(phrase(b"hello world").unwrap().1),
686 b"hello world".to_vec(),
687 );
688 assert_eq!(
691 print_to_vec(phrase(b"salut \"le\" monde").unwrap().1),
692 b"salut \"le\" monde".to_vec(),
693 );
694
695 let (rest, parsed) = phrase(b"fin\r\n du\r\nmonde").unwrap();
696 assert_eq!(rest, &b"\r\nmonde"[..]);
697 assert_eq!(&print_to_vec(parsed), b"fin du");
698
699 let (rest, parsed) = phrase(b"foo.bar").unwrap();
700 assert_eq!(rest, &b""[..]);
701 assert_eq!(&print_to_vec(parsed), b"foo \".\" bar");
702 }
703
704 #[test]
705 fn test_phrase_list() {
706 let (rest, parsed) = phrase_list(b",abc def,, ,ghi").unwrap();
707 assert_eq!(rest, &b""[..]);
708 assert_eq!(&print_to_vec(parsed.as_ref().unwrap()), b"abc def, ghi");
709 }
710
711 #[test]
712 fn test_unstructured() {
713 let (rest, parsed) = unstructured(b"").unwrap();
714 assert_eq!(rest, &b""[..]);
715 assert_eq!(parsed, Unstructured(vec![]));
716
717 let (rest, parsed) = unstructured(b" \t").unwrap();
718 assert_eq!(rest, &b""[..]);
719 assert_eq!(
720 parsed,
721 Unstructured(vec![UnstrToken::Plain(" \t"[..].into(), UnstrTxtKind::Fws)])
722 );
723
724 let (rest, parsed) = unstructured(b"foo =?UTF-8?q?foo?=").unwrap();
725 assert_eq!(rest, &b""[..]);
726 assert_eq!(
727 parsed,
728 Unstructured(vec![
729 UnstrToken::Plain("foo"[..].into(), UnstrTxtKind::Txt),
730 UnstrToken::Plain(" "[..].into(), UnstrTxtKind::Fws),
731 UnstrToken::Encoded(EncodedWord(vec![EncodedWordToken::Quoted(QuotedWord {
732 enc: EmailCharset::utf8(),
733 chunks: vec![QuotedChunk::Safe(b"foo"[..].into())],
734 })]))
735 ])
736 );
737
738 let (rest, parsed) = unstructured(b"foo=?UTF-8?q?foo?=").unwrap();
741 assert_eq!(rest, &b""[..]);
742 assert_eq!(
743 parsed,
744 Unstructured(vec![UnstrToken::Plain(
745 "foo=?UTF-8?q?foo?="[..].into(),
746 UnstrTxtKind::Txt
747 ),])
748 );
749
750 let (rest, parsed) = unstructured(b"foo\r\n\t").unwrap();
752 assert_eq!(rest, &b""[..]);
753 assert_eq!(
754 parsed,
755 Unstructured(vec![
756 UnstrToken::Plain("foo"[..].into(), UnstrTxtKind::Txt),
757 UnstrToken::Plain("\t"[..].into(), UnstrTxtKind::Fws),
758 ])
759 );
760 }
761}