eml_codec/text/
quoted.rs

1use nom::{
2    branch::alt,
3    bytes::complete::{tag, take, take_while1},
4    combinator::opt,
5    multi::many0,
6    sequence::{pair, preceded},
7    IResult,
8};
9
10use crate::text::ascii;
11use crate::text::whitespace::{cfws, fws, is_obs_no_ws_ctl};
12
13#[derive(Debug, PartialEq, Default, Clone)]
14pub struct QuotedString<'a>(pub Vec<&'a [u8]>);
15
16impl<'a> QuotedString<'a> {
17    pub fn push(&mut self, e: &'a [u8]) {
18        self.0.push(e)
19    }
20
21    pub fn to_string(&self) -> String {
22        let enc = encoding_rs::UTF_8;
23        let size = self.0.iter().fold(0, |acc, v| acc + v.len());
24
25        self.0
26            .iter()
27            .fold(String::with_capacity(size), |mut acc, v| {
28                let (content, _) = enc.decode_without_bom_handling(v);
29                acc.push_str(content.as_ref());
30                acc
31            })
32    }
33}
34
35/// Quoted pair
36///
37/// ```abnf
38///    quoted-pair     =   ("\" (VCHAR / WSP)) / obs-qp
39///    obs-qp          =   "\" (%d0 / obs-NO-WS-CTL / LF / CR)
40/// ```
41pub fn quoted_pair(input: &[u8]) -> IResult<&[u8], &[u8]> {
42    preceded(tag(&[ascii::BACKSLASH]), take(1usize))(input)
43}
44
45/// Allowed characters in quote
46///
47/// ```abnf
48///   qtext           =   %d33 /             ; Printable US-ASCII
49///                       %d35-91 /          ;  characters not including
50///                       %d93-126 /         ;  "\" or the quote character
51///                       obs-qtext
52/// ```
53fn is_restr_qtext(c: u8) -> bool {
54    c == ascii::EXCLAMATION
55        || (ascii::NUM..=ascii::LEFT_BRACKET).contains(&c)
56        || (ascii::RIGHT_BRACKET..=ascii::TILDE).contains(&c)
57}
58
59fn is_qtext(c: u8) -> bool {
60    is_restr_qtext(c) || is_obs_no_ws_ctl(c)
61}
62
63/// Quoted pair content
64///
65/// ```abnf
66///   qcontent        =   qtext / quoted-pair
67/// ```
68fn qcontent(input: &[u8]) -> IResult<&[u8], &[u8]> {
69    alt((take_while1(is_qtext), quoted_pair))(input)
70}
71
72/// Quoted string
73///
74/// ```abnf
75/// quoted-string   =   [CFWS]
76///                     DQUOTE *([FWS] qcontent) [FWS] DQUOTE
77///                     [CFWS]
78/// ```
79pub fn quoted_string(input: &[u8]) -> IResult<&[u8], QuotedString> {
80    let (input, _) = opt(cfws)(input)?;
81    let (input, _) = tag("\"")(input)?;
82    let (input, content) = many0(pair(opt(fws), qcontent))(input)?;
83
84    // Rebuild string
85    let mut qstring = content
86        .iter()
87        .fold(QuotedString::default(), |mut acc, (maybe_wsp, c)| {
88            if maybe_wsp.is_some() {
89                acc.push(&[ascii::SP]);
90            }
91            acc.push(c);
92            acc
93        });
94
95    let (input, maybe_wsp) = opt(fws)(input)?;
96    if maybe_wsp.is_some() {
97        qstring.push(&[ascii::SP]);
98    }
99
100    let (input, _) = tag("\"")(input)?;
101    let (input, _) = opt(cfws)(input)?;
102    Ok((input, qstring))
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108
109    #[test]
110    fn test_quoted_string_parser() {
111        assert_eq!(
112            quoted_string(b" \"hello\\\"world\" ").unwrap().1,
113            QuotedString(vec![b"hello", &[ascii::DQUOTE], b"world"])
114        );
115
116        assert_eq!(
117            quoted_string(b"\"hello\r\n world\""),
118            Ok((
119                &b""[..],
120                QuotedString(vec![b"hello", &[ascii::SP], b"world"])
121            )),
122        );
123    }
124
125    use crate::text::ascii;
126
127    #[test]
128    fn test_quoted_string_object() {
129        assert_eq!(
130            QuotedString(vec![b"hello", &[ascii::SP], b"world"]).to_string(),
131            "hello world".to_string(),
132        );
133    }
134}