gix_mailmap/
parse.rs

1mod error {
2    use bstr::BString;
3
4    /// The error returned by [`parse()`][crate::parse()].
5    #[derive(Debug, thiserror::Error)]
6    #[allow(missing_docs)]
7    pub enum Error {
8        #[error("Line {line_number} has too many names or emails, or none at all: {line:?}")]
9        UnconsumedInput { line_number: usize, line: BString },
10        #[error("{line_number}: {line:?}: {message}")]
11        Malformed {
12            line_number: usize,
13            line: BString,
14            message: String,
15        },
16    }
17}
18
19use bstr::{BStr, ByteSlice};
20pub use error::Error;
21
22use crate::Entry;
23
24/// An iterator to parse mailmap lines on-demand.
25pub struct Lines<'a> {
26    lines: bstr::Lines<'a>,
27    line_no: usize,
28}
29
30impl<'a> Lines<'a> {
31    pub(crate) fn new(input: &'a [u8]) -> Self {
32        Lines {
33            lines: input.as_bstr().lines(),
34            line_no: 0,
35        }
36    }
37}
38
39impl<'a> Iterator for Lines<'a> {
40    type Item = Result<Entry<'a>, Error>;
41
42    fn next(&mut self) -> Option<Self::Item> {
43        for line in self.lines.by_ref() {
44            self.line_no += 1;
45            match line.first() {
46                None => continue,
47                Some(b) if *b == b'#' => continue,
48                Some(_) => {}
49            }
50            let line = line.trim();
51            if line.is_empty() {
52                continue;
53            }
54            return parse_line(line.into(), self.line_no).into();
55        }
56        None
57    }
58}
59
60fn parse_line(line: &BStr, line_number: usize) -> Result<Entry<'_>, Error> {
61    let (name1, email1, rest) = parse_name_and_email(line, line_number)?;
62    let (name2, email2, rest) = parse_name_and_email(rest, line_number)?;
63    if !rest.trim().is_empty() {
64        return Err(Error::UnconsumedInput {
65            line_number,
66            line: line.into(),
67        });
68    }
69    Ok(match (name1, email1, name2, email2) {
70        (Some(proper_name), Some(commit_email), None, None) => Entry::change_name_by_email(proper_name, commit_email),
71        (None, Some(proper_email), None, Some(commit_email)) => {
72            Entry::change_email_by_email(proper_email, commit_email)
73        }
74        (Some(proper_name), Some(proper_email), None, Some(commit_email)) => {
75            Entry::change_name_and_email_by_email(proper_name, proper_email, commit_email)
76        }
77        (Some(proper_name), Some(proper_email), Some(commit_name), Some(commit_email)) => {
78            Entry::change_name_and_email_by_name_and_email(proper_name, proper_email, commit_name, commit_email)
79        }
80        (None, Some(proper_email), Some(commit_name), Some(commit_email)) => {
81            Entry::change_email_by_name_and_email(proper_email, commit_name, commit_email)
82        }
83        _ => {
84            return Err(Error::Malformed {
85                line_number,
86                line: line.into(),
87                message: "Emails without a name or email to map to are invalid".into(),
88            })
89        }
90    })
91}
92
93fn parse_name_and_email(
94    line: &BStr,
95    line_number: usize,
96) -> Result<(Option<&'_ BStr>, Option<&'_ BStr>, &'_ BStr), Error> {
97    match line.find_byte(b'<') {
98        Some(start_bracket) => {
99            let email = &line[start_bracket + 1..];
100            let closing_bracket = email.find_byte(b'>').ok_or_else(|| Error::Malformed {
101                line_number,
102                line: line.into(),
103                message: "Missing closing bracket '>' in email".into(),
104            })?;
105            let email = email[..closing_bracket].trim().as_bstr();
106            if email.is_empty() {
107                return Err(Error::Malformed {
108                    line_number,
109                    line: line.into(),
110                    message: "Email must not be empty".into(),
111                });
112            }
113            let name = line[..start_bracket].trim().as_bstr();
114            let rest = line[start_bracket + closing_bracket + 2..].as_bstr();
115            Ok(((!name.is_empty()).then_some(name), Some(email), rest))
116        }
117        None => Ok((None, None, line)),
118    }
119}