Skip to main content

eml_codec/text/
words.rs

1use crate::i18n::ContainsUtf8;
2use crate::print::{Formatter, Print, ToStringFromPrint};
3use crate::text::ascii;
4use crate::text::utf8::{is_nonascii_or, take_utf8_while1};
5use crate::text::whitespace::cfws;
6#[cfg(feature = "arbitrary")]
7use crate::{
8    arbitrary_utils::{arbitrary_string_nonempty_where, arbitrary_vec_nonempty_where},
9    fuzz_eq::FuzzEq,
10};
11#[cfg(feature = "arbitrary")]
12use arbitrary::Arbitrary;
13use bounded_static::ToStatic;
14use eml_codec_derives::instrument_input;
15use nom::{
16    bytes::complete::{tag, take_while1},
17    character::is_alphanumeric,
18    combinator::{map, opt, recognize},
19    multi::many0,
20    sequence::{delimited, pair},
21    IResult,
22};
23use std::borrow::Cow;
24use std::fmt;
25
26/// Printable characters
27///
28/// following RFC6532, this includes non-ascii UTF8 text
29pub fn is_vchar(c: char) -> bool {
30    is_nonascii_or(|c| (ascii::EXCLAMATION..=ascii::TILDE).contains(&c))(c)
31}
32
33/// A MIME atom.
34// Contains a non-zero amount of bytes that satisfy `is_mime_atom_text`.
35#[derive(Clone, ContainsUtf8, PartialEq, Default, ToStatic, ToStringFromPrint)]
36#[contains_utf8(false)]
37pub struct MIMEAtom<'a>(pub Cow<'a, [u8]>);
38
39impl<'a> fmt::Debug for MIMEAtom<'a> {
40    fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
41        fmt.debug_tuple("MIMEAtom")
42            .field(&String::from_utf8_lossy(&self.0))
43            .finish()
44    }
45}
46impl<'a> Print for MIMEAtom<'a> {
47    fn print(&self, fmt: &mut impl Formatter) {
48        fmt.write_bytes(&self.0)
49    }
50}
51#[cfg(feature = "arbitrary")]
52impl<'a, 'b> Arbitrary<'a> for MIMEAtom<'b> {
53    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
54        let bytes = arbitrary_vec_nonempty_where(u, |c| is_mime_atom_text(*c), b'X')?;
55        Ok(MIMEAtom(Cow::Owned(bytes)))
56    }
57}
58#[cfg(feature = "arbitrary")]
59impl<'a> FuzzEq for MIMEAtom<'a> {
60    fn fuzz_eq(&self, other: &Self) -> bool {
61        self == other
62    }
63}
64impl<'a> MIMEAtom<'a> {
65    pub fn chars<'b>(&'b self) -> MIMEAtomChars<'a, 'b> {
66        MIMEAtomChars { a: self, idx: 0 }
67    }
68}
69#[derive(Clone)]
70pub struct MIMEAtomChars<'a, 'b> {
71    a: &'b MIMEAtom<'a>,
72    idx: usize,
73}
74impl<'a, 'b> Iterator for MIMEAtomChars<'a, 'b> {
75    type Item = char;
76    fn next(&mut self) -> Option<Self::Item> {
77        if self.idx < self.a.0.len() {
78            let c: u8 = self.a.0[self.idx];
79            self.idx += 1;
80            Some(c.into())
81        } else {
82            None
83        }
84    }
85}
86
87/// MIME Token allowed characters
88///
89/// forbidden: ()<>@,;:\"/[]?=
90pub fn is_mime_atom_text(c: u8) -> bool {
91    is_alphanumeric(c)
92        || c == ascii::EXCLAMATION
93        || c == ascii::NUM
94        || c == ascii::DOLLAR
95        || c == ascii::PERCENT
96        || c == ascii::AMPERSAND
97        || c == ascii::SQUOTE
98        || c == ascii::ASTERISK
99        || c == ascii::PLUS
100        || c == ascii::MINUS
101        || c == ascii::PERIOD
102        || c == ascii::CARET
103        || c == ascii::UNDERSCORE
104        || c == ascii::GRAVE
105        || c == ascii::LEFT_CURLY
106        || c == ascii::PIPE
107        || c == ascii::RIGHT_CURLY
108        || c == ascii::TILDE
109}
110
111/// MIME Token
112///
113/// `[CFWS] 1*token_text [CFWS]`
114#[instrument_input("tracing")]
115pub fn mime_atom(input: &[u8]) -> IResult<&[u8], MIMEAtom<'_>> {
116    delimited(opt(cfws), mime_atom_plain, opt(cfws))(input)
117}
118
119/// `1*token_text`
120pub fn mime_atom_plain(input: &[u8]) -> IResult<&[u8], MIMEAtom<'_>> {
121    map(take_while1(is_mime_atom_text), |b: &[u8]| {
122        MIMEAtom(b.into())
123    })(input)
124}
125
126/// An IMF atom.
127// Contains a non-zero amount of bytes that satisfy `is_atext`.
128#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic, ToStringFromPrint)]
129pub struct Atom<'a>(pub Cow<'a, str>);
130
131impl<'a> Print for Atom<'a> {
132    fn print(&self, fmt: &mut impl Formatter) {
133        fmt.write_bytes(self.0.as_bytes())
134    }
135}
136impl<'a> TryFrom<&'a str> for Atom<'a> {
137    type Error = (); // TODO
138    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
139        if s.chars().all(is_atext) && !s.is_empty() {
140            Ok(Atom(s.into()))
141        } else {
142            Err(())
143        }
144    }
145}
146#[cfg(feature = "arbitrary")]
147impl<'a> Arbitrary<'a> for Atom<'a> {
148    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
149        let bytes = arbitrary_string_nonempty_where(u, is_atext, 'X')?;
150        Ok(Atom(Cow::Owned(bytes)))
151    }
152}
153#[cfg(feature = "arbitrary")]
154impl<'a> FuzzEq for Atom<'a> {
155    fn fuzz_eq(&self, other: &Self) -> bool {
156        self == other
157    }
158}
159
160/// Atom allowed characters
161///
162/// authorized: !#$%&'*+-/=?^_`{|}~
163///
164/// following RFC6532, atext also allows non-ascii UTF8 characters
165pub fn is_atext(c: char) -> bool {
166    is_nonascii_or(|c| {
167        is_alphanumeric(c)
168            || c == ascii::EXCLAMATION
169            || c == ascii::NUM
170            || c == ascii::DOLLAR
171            || c == ascii::PERCENT
172            || c == ascii::AMPERSAND
173            || c == ascii::SQUOTE
174            || c == ascii::ASTERISK
175            || c == ascii::PLUS
176            || c == ascii::MINUS
177            || c == ascii::SLASH
178            || c == ascii::EQ
179            || c == ascii::QUESTION
180            || c == ascii::CARET
181            || c == ascii::UNDERSCORE
182            || c == ascii::GRAVE
183            || c == ascii::LEFT_CURLY
184            || c == ascii::PIPE
185            || c == ascii::RIGHT_CURLY
186            || c == ascii::TILDE
187    })(c)
188}
189
190/// Atom
191///
192/// `[CFWS] 1*atext [CFWS]`
193#[instrument_input("tracing")]
194pub fn atom(input: &[u8]) -> IResult<&[u8], Atom<'_>> {
195    map(
196        delimited(opt(cfws), take_utf8_while1(is_atext), opt(cfws)),
197        Atom,
198    )(input)
199}
200
201/// An IMF dot-atom.
202// Only contains bytes that satisfy is_atext or are '.'.
203#[derive(Clone, ContainsUtf8, Debug, PartialEq, ToStatic, ToStringFromPrint)]
204pub struct DotAtom<'a>(pub Cow<'a, str>);
205
206impl<'a> Print for DotAtom<'a> {
207    fn print(&self, fmt: &mut impl Formatter) {
208        fmt.write_bytes(self.0.as_bytes())
209    }
210}
211#[cfg(feature = "arbitrary")]
212impl<'a> Arbitrary<'a> for DotAtom<'a> {
213    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
214        let mut s = arbitrary_string_nonempty_where(u, is_atext, 'X')?;
215        for _ in 0..u.int_in_range(0..=3)? {
216            s.push('.');
217            s.push_str(&arbitrary_string_nonempty_where(u, is_atext, 'X')?);
218        }
219        Ok(DotAtom(Cow::Owned(s)))
220    }
221}
222#[cfg(feature = "arbitrary")]
223impl<'a> FuzzEq for DotAtom<'a> {
224    fn fuzz_eq(&self, other: &Self) -> bool {
225        self == other
226    }
227}
228
229/// dot-atom-text
230///
231/// `1*atext *("." 1*atext)`
232pub fn dot_atom_text(input: &[u8]) -> IResult<&[u8], DotAtom<'_>> {
233    map(
234        recognize(pair(
235            take_utf8_while1(is_atext),
236            many0(pair(tag("."), take_utf8_while1(is_atext))),
237        )),
238        |b: &[u8]| {
239            // SAFETY: `b` is composed of bytes recognized by
240            // `take_utf8_while1()` and dots ("."). Both are guaranteed to be
241            // valid UTF-8.
242            let s = unsafe { str::from_utf8_unchecked(b) };
243            DotAtom(s.into())
244        },
245    )(input)
246}
247
248/// dot-atom
249///
250/// `[CFWS] dot-atom-text [CFWS]`
251#[instrument_input("tracing")]
252pub fn dot_atom(input: &[u8]) -> IResult<&[u8], DotAtom<'_>> {
253    delimited(opt(cfws), dot_atom_text, opt(cfws))(input)
254}
255
256#[cfg(test)]
257mod tests {
258    use super::*;
259
260    #[test]
261    fn test_atext() {
262        assert!(is_atext('='));
263        assert!(is_atext('5'));
264        assert!(is_atext('Q'));
265        assert!(!is_atext(' '));
266        assert!(is_atext('É')); // non-ascii UTF8 is allowed (RFC6532)
267    }
268
269    #[test]
270    fn test_atom() {
271        assert_eq!(
272            atom(b"(skip)  imf_codec (hidden) aerogramme"),
273            Ok((&b"aerogramme"[..], Atom("imf_codec".into())))
274        );
275    }
276
277    #[test]
278    fn test_dot_atom_text() {
279        assert_eq!(
280            dot_atom_text(b"quentin.dufour.io abcdef"),
281            Ok((&b" abcdef"[..], DotAtom("quentin.dufour.io".into())))
282        );
283    }
284
285    #[test]
286    fn test_dot_atom() {
287        assert_eq!(
288            dot_atom(b"   (skip) quentin.dufour.io abcdef"),
289            Ok((&b"abcdef"[..], DotAtom("quentin.dufour.io".into())))
290        );
291    }
292}