Skip to main content

addr_spec/
parser.rs

1use alloc::string::String;
2use core::{error::Error, fmt, mem::ManuallyDrop, str::Chars};
3
4use super::unicode;
5use super::AddrSpec;
6
7pub const fn is_ascii_control_and_not_htab(chr: char) -> bool {
8    chr.is_ascii_control() && chr != '\t'
9}
10
11pub const fn is_ascii_control_or_space(chr: char) -> bool {
12    chr.is_ascii_control() || chr == ' '
13}
14
15pub const fn is_not_atext(chr: char) -> bool {
16    is_ascii_control_or_space(chr)
17        || matches!(
18            chr,
19            '"' | '(' | ')' | ',' | ':' | '<' | '>' | '@' | '[' | ']' | '\\'
20        )
21}
22
23pub const fn is_not_dtext(chr: char) -> bool {
24    is_ascii_control_or_space(chr) || matches!(chr, '[' | ']' | '\\')
25}
26
27/// A error that can occur when parsing or creating an address specification.
28#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
29pub struct ParseError(pub(super) &'static str, pub(super) usize);
30
31impl ParseError {
32    /// Returns a static error message.
33    #[inline]
34    pub fn message(&self) -> &'static str {
35        self.0
36    }
37
38    /// Returns the byte index where the error occurred.
39    #[inline]
40    pub fn index(&self) -> usize {
41        self.1
42    }
43}
44
45impl fmt::Display for ParseError {
46    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(
48            formatter,
49            "parse error at index {}: {}",
50            self.message(),
51            self.index()
52        )
53    }
54}
55
56impl Error for ParseError {}
57
58pub struct Parser<'a> {
59    input: &'a str,
60    iterator: Chars<'a>,
61}
62
63impl<'a> Parser<'a> {
64    #[inline]
65    pub fn new(input: &'a str) -> Parser<'a> {
66        Parser {
67            input,
68            iterator: input.chars(),
69        }
70    }
71
72    pub fn parse(mut self) -> Result<AddrSpec, ParseError> {
73        #[cfg(feature = "white-spaces")]
74        self.parse_cfws()?;
75        let local_part = self.parse_local_part()?;
76        #[cfg(feature = "white-spaces")]
77        self.parse_cfws()?;
78        self.skip_at()?;
79        #[cfg(feature = "white-spaces")]
80        self.parse_cfws()?;
81        // `literal` only used when feature is enabled
82        #[allow(unused_variables)]
83        let (domain, literal) = self.parse_domain()?;
84        #[cfg(feature = "white-spaces")]
85        self.parse_cfws()?;
86        self.check_end("expected end of address")?;
87        Ok(AddrSpec {
88            local_part,
89            domain,
90            #[cfg(feature = "literals")]
91            literal,
92        })
93    }
94
95    #[cfg(feature = "white-spaces")]
96    fn parse_cfws(&mut self) -> Result<(), ParseError> {
97        self.skip_fws();
98        #[cfg(feature = "comments")]
99        while self.eat_chr('(') {
100            self.parse_comment()?;
101            self.skip_fws();
102        }
103        Ok(())
104    }
105
106    #[cfg(feature = "white-spaces")]
107    fn skip_fws(&mut self) {
108        self.skip_ws();
109        if !self.eat_str("\r\n") {
110            return;
111        }
112        self.skip_ws();
113    }
114
115    #[cfg(feature = "white-spaces")]
116    fn skip_ws(&mut self) {
117        loop {
118            if !self.eat_slice([' ', '\t']) {
119                break;
120            }
121        }
122    }
123
124    #[cfg(feature = "white-spaces")]
125    fn eat_slice<const N: usize>(&mut self, pattern: [char; N]) -> bool {
126        if self.iterator.as_str().starts_with(pattern) {
127            self.iterator.next();
128            return true;
129        }
130        false
131    }
132
133    #[cfg(feature = "white-spaces")]
134    fn eat_str(&mut self, pattern: &str) -> bool {
135        if let Some(input) = self.iterator.as_str().strip_prefix(pattern) {
136            self.iterator = input.chars();
137            return true;
138        }
139        false
140    }
141
142    fn eat_chr(&mut self, pattern: char) -> bool {
143        if self.iterator.as_str().starts_with(pattern) {
144            self.iterator.next();
145            return true;
146        }
147        false
148    }
149
150    #[cfg(feature = "comments")]
151    fn parse_comment(&mut self) -> Result<(), ParseError> {
152        #[cfg(feature = "white-spaces")]
153        self.skip_fws();
154
155        let mut nest_level = 1usize;
156        while let Some(chr) = self.iterator.next() {
157            match chr {
158                ')' => {
159                    if nest_level == 1 {
160                        return Ok(());
161                    }
162                    nest_level -= 1;
163                }
164                '\\' => {
165                    self.parse_quoted_pair()?;
166                }
167                '(' => {
168                    nest_level += 1;
169                }
170                chr => {
171                    if is_ascii_control_or_space(chr) {
172                        return Err(self.error("invalid character in comment", -1));
173                    }
174                }
175            }
176
177            #[cfg(feature = "white-spaces")]
178            self.skip_fws();
179        }
180
181        Err(self.error("expected ')' for comment", 0))
182    }
183
184    fn parse_quoted_pair(&mut self) -> Result<char, ParseError> {
185        match self.iterator.next() {
186            Some(chr) if !is_ascii_control_and_not_htab(chr) => Ok(chr),
187            Some(_) => Err(self.error("invalid character in quoted pair", -1)),
188            None => Err(self.error("unexpected end of quoted pair", 0)),
189        }
190    }
191
192    fn parse_local_part(&mut self) -> Result<String, ParseError> {
193        if !self.eat_chr('"') {
194            return Ok(unicode::normalize(
195                self.parse_dot_atom("empty label in local part")?,
196            ));
197        }
198        Ok(unicode::normalize(self.parse_quoted_string(
199            "invalid character in quoted local part",
200            "expected '\"' for quoted local part",
201        )?))
202    }
203
204    pub fn parse_dot_atom(
205        &mut self,
206        empty_label_error_text: &'static str,
207    ) -> Result<&str, ParseError> {
208        let input = self.iterator.as_str();
209        let size = input.find(is_not_atext).unwrap_or(input.len());
210
211        let dot_atom = &input[..size];
212        if let Some(offset) = dot_atom
213            .split('.')
214            .find(|label| label.is_empty())
215            .map(|label| label.as_ptr() as usize - dot_atom.as_ptr() as usize)
216        {
217            return Err(self.error(empty_label_error_text, offset as isize));
218        }
219
220        self.iterator = input[size..].chars();
221        Ok(dot_atom)
222    }
223
224    fn parse_quoted_string(
225        &mut self,
226        invalid_character_error_text: &'static str,
227        expected_quote_error_text: &'static str,
228    ) -> Result<String, ParseError> {
229        #[cfg(feature = "white-spaces")]
230        self.skip_fws();
231
232        let mut quoted_string = unsafe { FixedVec::new(self.iterator.as_str().len()) };
233        while let Some(chr) = self.iterator.next() {
234            let chr = match chr {
235                '"' => return Ok(quoted_string.into()),
236                '\\' => self.parse_quoted_pair()?,
237                chr if is_ascii_control_or_space(chr) => {
238                    return Err(self.error(invalid_character_error_text, -1))
239                }
240                chr => chr,
241            };
242            unsafe {
243                quoted_string.extend_char_unchecked(chr);
244            }
245
246            #[cfg(feature = "white-spaces")]
247            self.skip_fws();
248        }
249
250        Err(self.error(expected_quote_error_text, 0))
251    }
252
253    fn skip_at(&mut self) -> Result<(), ParseError> {
254        if self.eat_chr('@') {
255            return Ok(());
256        }
257        Err(self.error("expected '@'", 1))
258    }
259
260    fn parse_domain(&mut self) -> Result<(String, bool), ParseError> {
261        #[cfg(feature = "literals")]
262        if self.eat_chr('[') {
263            return Ok((unicode::normalize(self.parse_domain_literal()?), true));
264        }
265        Ok((
266            unicode::normalize(self.parse_dot_atom("empty label in domain")?),
267            false,
268        ))
269    }
270
271    #[cfg(all(feature = "literals", not(feature = "white-spaces")))]
272    fn parse_domain_literal(&mut self) -> Result<&str, ParseError> {
273        let input = self.iterator.as_str();
274        let size = input.find(is_not_dtext).unwrap_or(input.len());
275
276        self.iterator = input[size..].chars();
277        if !self.eat_chr(']') {
278            return Err(self.error("expected ']' for domain literal", 0));
279        }
280
281        Ok(&input[..size])
282    }
283
284    #[cfg(all(feature = "literals", feature = "white-spaces"))]
285    fn parse_domain_literal(&mut self) -> Result<String, ParseError> {
286        #[cfg(feature = "white-spaces")]
287        self.skip_fws();
288
289        let mut domain = unsafe { FixedVec::new(self.iterator.as_str().len()) };
290        while let Some(chr) = self.iterator.next() {
291            let chr = match chr {
292                ']' => return Ok(domain.into()),
293                chr if is_not_dtext(chr) => {
294                    return Err(self.error("invalid character in literal domain", -1))
295                }
296                chr => chr,
297            };
298            unsafe {
299                domain.extend_char_unchecked(chr);
300            }
301
302            #[cfg(feature = "white-spaces")]
303            self.skip_fws();
304        }
305
306        Err(self.error("expected ']' for domain literal", 0))
307    }
308
309    #[inline]
310    pub fn check_end(self, message: &'static str) -> Result<(), ParseError> {
311        if self.iterator.as_str().is_empty() {
312            return Ok(());
313        }
314        Err(self.error(message, 0))
315    }
316
317    fn error(&self, message: &'static str, offset: isize) -> ParseError {
318        ParseError(
319            message,
320            (self.input.len() - self.iterator.as_str().len())
321                .checked_add_signed(offset)
322                .unwrap(),
323        )
324    }
325}
326
327pub struct FixedVec<T> {
328    ptr: *mut T,
329    len: usize,
330    cap: usize,
331}
332
333impl<T> FixedVec<T> {
334    pub unsafe fn new(cap: usize) -> Self {
335        Self {
336            ptr: unsafe {
337                alloc::alloc::alloc(alloc::alloc::Layout::array::<T>(cap).unwrap()).cast()
338            },
339            len: 0,
340            cap,
341        }
342    }
343
344    unsafe fn extend_unchecked(&mut self, slice: &[T]) {
345        unsafe {
346            core::ptr::copy_nonoverlapping(slice.as_ptr(), self.ptr.add(self.len), slice.len());
347        }
348        self.len += slice.len();
349        debug_assert!(self.len <= self.cap);
350    }
351}
352
353impl FixedVec<u8> {
354    unsafe fn extend_char_unchecked(&mut self, chr: char) {
355        self.extend_unchecked(chr.encode_utf8(&mut [0; 4]).as_bytes())
356    }
357}
358
359impl<T> Drop for FixedVec<T> {
360    fn drop(&mut self) {
361        unsafe {
362            alloc::alloc::dealloc(
363                self.ptr.cast(),
364                alloc::alloc::Layout::array::<T>(self.cap).unwrap(),
365            )
366        }
367    }
368}
369
370impl From<FixedVec<u8>> for String {
371    fn from(val: FixedVec<u8>) -> Self {
372        let val = ManuallyDrop::new(val);
373        unsafe { String::from_raw_parts(val.ptr, val.len, val.cap) }
374    }
375}
376
377#[cfg(test)]
378mod tests {
379    mod dot_atoms {
380        use super::super::{ParseError, Parser};
381        use alloc::string::ToString;
382
383        #[test]
384        fn test_parse_local_part() {
385            assert_eq!(&Parser::new("test").parse_local_part().unwrap(), "test")
386        }
387
388        #[test]
389        fn test_parse_empty_local_part() {
390            assert_eq!(
391                Parser::new("").parse_local_part().unwrap_err(),
392                ParseError("empty label in local part", 0)
393            )
394        }
395
396        #[test]
397        fn test_parse_local_part_with_empty_label_in_front() {
398            assert_eq!(
399                Parser::new(".test").parse_local_part().unwrap_err(),
400                ParseError("empty label in local part", 0)
401            )
402        }
403
404        #[test]
405        fn test_parse_local_part_with_empty_label_in_middle() {
406            assert_eq!(
407                Parser::new("te..st").parse_local_part().unwrap_err(),
408                ParseError("empty label in local part", 3)
409            )
410        }
411
412        #[test]
413        fn test_parse_local_part_with_empty_label_in_back() {
414            assert_eq!(
415                Parser::new("test.").parse_local_part().unwrap_err(),
416                ParseError("empty label in local part", 5)
417            )
418        }
419
420        #[test]
421        fn test_parse_domain() {
422            assert_eq!(
423                Parser::new("test").parse_domain().unwrap(),
424                ("test".to_string(), false)
425            )
426        }
427
428        #[test]
429        fn test_parse_empty_domain() {
430            assert_eq!(
431                Parser::new("").parse_domain().unwrap_err(),
432                ParseError("empty label in domain", 0)
433            )
434        }
435
436        #[test]
437        fn test_parse_domain_with_empty_label_in_front() {
438            assert_eq!(
439                Parser::new(".test").parse_domain().unwrap_err(),
440                ParseError("empty label in domain", 0)
441            )
442        }
443
444        #[test]
445        fn test_parse_domain_with_empty_label_in_middle() {
446            assert_eq!(
447                Parser::new("te..st").parse_domain().unwrap_err(),
448                ParseError("empty label in domain", 3)
449            )
450        }
451
452        #[test]
453        fn test_parse_domain_with_empty_label_in_back() {
454            assert_eq!(
455                Parser::new("test.").parse_domain().unwrap_err(),
456                ParseError("empty label in domain", 5)
457            )
458        }
459    }
460
461    #[cfg(feature = "literals")]
462    mod literals {
463        use super::super::{ParseError, Parser};
464        use alloc::string::ToString;
465
466        #[test]
467        fn test_parse_literal_domain() {
468            assert_eq!(
469                Parser::new("[test]").parse_domain().unwrap(),
470                ("test".to_string(), true)
471            )
472        }
473
474        #[test]
475        fn test_parse_literal_domain_without_bracket() {
476            assert_eq!(
477                Parser::new("[test").parse_domain().unwrap_err(),
478                ParseError("expected ']' for domain literal", 5)
479            )
480        }
481
482        #[test]
483        fn test_parse_empty_literal_domain() {
484            assert_eq!(
485                Parser::new("[]").parse_domain().unwrap(),
486                ("".to_string(), true)
487            )
488        }
489
490        #[test]
491        fn test_parse_empty_literal_domain_without_bracket() {
492            assert_eq!(
493                Parser::new("[").parse_domain().unwrap_err(),
494                ParseError("expected ']' for domain literal", 1)
495            )
496        }
497
498        #[cfg(not(feature = "white-spaces"))]
499        #[test]
500        fn test_parse_literal_domain_with_white_spaces() {
501            assert_eq!(
502                Parser::new("[te st]").parse_domain().unwrap_err(),
503                ParseError("expected ']' for domain literal", 3)
504            )
505        }
506
507        #[cfg(feature = "white-spaces")]
508        #[test]
509        fn test_parse_literal_domain_with_white_spaces() {
510            assert_eq!(
511                Parser::new("[te st]").parse_domain().unwrap(),
512                ("test".to_string(), true)
513            )
514        }
515
516        #[cfg(feature = "white-spaces")]
517        #[test]
518        fn test_parse_literal_domain_with_fws_in_front() {
519            assert_eq!(
520                Parser::new("[\r\ntest]").parse_domain().unwrap(),
521                ("test".to_string(), true)
522            )
523        }
524
525        #[cfg(feature = "white-spaces")]
526        #[test]
527        fn test_parse_literal_domain_with_fws_in_middle() {
528            assert_eq!(
529                Parser::new("[te\r\nst]").parse_domain().unwrap(),
530                ("test".to_string(), true)
531            )
532        }
533
534        #[cfg(feature = "white-spaces")]
535        #[test]
536        fn test_parse_literal_domain_with_fws_in_back() {
537            assert_eq!(
538                Parser::new("[test\r\n]").parse_domain().unwrap(),
539                ("test".to_string(), true)
540            )
541        }
542    }
543}