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#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
29pub struct ParseError(pub(super) &'static str, pub(super) usize);
30
31impl ParseError {
32 #[inline]
34 pub fn message(&self) -> &'static str {
35 self.0
36 }
37
38 #[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 #[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}