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
26pub fn is_vchar(c: char) -> bool {
30 is_nonascii_or(|c| (ascii::EXCLAMATION..=ascii::TILDE).contains(&c))(c)
31}
32
33#[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
87pub 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#[instrument_input("tracing")]
115pub fn mime_atom(input: &[u8]) -> IResult<&[u8], MIMEAtom<'_>> {
116 delimited(opt(cfws), mime_atom_plain, opt(cfws))(input)
117}
118
119pub 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#[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 = (); 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
160pub 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#[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#[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
229pub 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 let s = unsafe { str::from_utf8_unchecked(b) };
243 DotAtom(s.into())
244 },
245 )(input)
246}
247
248#[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('É')); }
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}