Skip to main content

gix_mailmap/
parse.rs

1/// The error returned by [`parse()`](crate::parse()).
2pub type Error = gix_error::Exn<gix_error::ValidationError>;
3
4use bstr::{BStr, ByteSlice};
5use gix_error::{ErrorExt, OptionExt, ValidationError};
6
7use crate::Entry;
8
9/// An iterator to parse mailmap lines on-demand.
10pub struct Lines<'a> {
11    lines: bstr::Lines<'a>,
12    line_no: usize,
13}
14
15impl<'a> Lines<'a> {
16    pub(crate) fn new(input: &'a [u8]) -> Self {
17        Lines {
18            lines: input.as_bstr().lines(),
19            line_no: 0,
20        }
21    }
22}
23
24impl<'a> Iterator for Lines<'a> {
25    type Item = Result<Entry<'a>, Error>;
26
27    fn next(&mut self) -> Option<Self::Item> {
28        for line in self.lines.by_ref() {
29            self.line_no += 1;
30            match line.first() {
31                None => continue,
32                Some(b) if *b == b'#' => continue,
33                Some(_) => {}
34            }
35            let line = line.trim();
36            if line.is_empty() {
37                continue;
38            }
39            return parse_line(line.into(), self.line_no).into();
40        }
41        None
42    }
43}
44
45fn parse_line(line: &BStr, line_number: usize) -> Result<Entry<'_>, Error> {
46    let (name1, email1, rest) = parse_name_and_email(line, line_number, false)?;
47    let (name2, email2, _rest) = parse_name_and_email(rest, line_number, true).unwrap_or((None, None, rest));
48    if email1.is_none() {
49        return Err(
50            ValidationError::new_with_input(format!("Line {line_number} does not contain an email"), line).raise(),
51        );
52    }
53    Ok(match (name1, email1, name2, email2) {
54        (Some(proper_name), Some(commit_email), None, None) => Entry::change_name_by_email(proper_name, commit_email),
55        (None, Some(proper_email), None, Some(commit_email)) => {
56            Entry::change_email_by_email(proper_email, commit_email)
57        }
58        (Some(proper_name), Some(proper_email), None, Some(commit_email)) => {
59            Entry::change_name_and_email_by_email(proper_name, proper_email, commit_email)
60        }
61        (Some(proper_name), Some(proper_email), Some(commit_name), Some(commit_email)) => {
62            Entry::change_name_and_email_by_name_and_email(proper_name, proper_email, commit_name, commit_email)
63        }
64        (None, Some(proper_email), Some(commit_name), Some(commit_email)) => {
65            Entry::change_email_by_name_and_email(proper_email, commit_name, commit_email)
66        }
67        _ => {
68            return Err(ValidationError::new_with_input(
69                format!("{line_number}: Emails without a name or email to map to are invalid"),
70                line,
71            )
72            .raise());
73        }
74    })
75}
76
77fn parse_name_and_email(
78    line: &BStr,
79    line_number: usize,
80    allow_empty_email: bool,
81) -> Result<(Option<&'_ BStr>, Option<&'_ BStr>, &'_ BStr), Error> {
82    match line.find_byte(b'<') {
83        Some(start_bracket) => {
84            let email = &line[start_bracket + 1..];
85            let closing_bracket = email.find_byte(b'>').ok_or_raise(|| {
86                ValidationError::new_with_input(format!("{line_number}: Missing closing bracket '>' in email"), line)
87            })?;
88            let email = email[..closing_bracket].trim().as_bstr();
89            if email.is_empty() && !allow_empty_email {
90                return Err(
91                    ValidationError::new_with_input(format!("{line_number}: Email must not be empty"), line).raise(),
92                );
93            }
94            let name = line[..start_bracket].trim().as_bstr();
95            let rest = line[start_bracket + closing_bracket + 2..].as_bstr();
96            Ok(((!name.is_empty()).then_some(name), Some(email), rest))
97        }
98        None => Ok((None, None, line)),
99    }
100}