| mod error { |
| use bstr::BString; |
| |
| /// The error returned by [`parse()`][crate::parse()]. |
| #[derive(Debug, thiserror::Error)] |
| #[allow(missing_docs)] |
| pub enum Error { |
| #[error("Line {line_number} has too many names or emails, or none at all: {line:?}")] |
| UnconsumedInput { line_number: usize, line: BString }, |
| #[error("{line_number}: {line:?}: {message}")] |
| Malformed { |
| line_number: usize, |
| line: BString, |
| message: String, |
| }, |
| } |
| } |
| |
| use bstr::{BStr, ByteSlice}; |
| pub use error::Error; |
| |
| use crate::Entry; |
| |
| /// An iterator to parse mailmap lines on-demand. |
| pub struct Lines<'a> { |
| lines: bstr::Lines<'a>, |
| line_no: usize, |
| } |
| |
| impl<'a> Lines<'a> { |
| pub(crate) fn new(input: &'a [u8]) -> Self { |
| Lines { |
| lines: input.as_bstr().lines(), |
| line_no: 0, |
| } |
| } |
| } |
| |
| impl<'a> Iterator for Lines<'a> { |
| type Item = Result<Entry<'a>, Error>; |
| |
| fn next(&mut self) -> Option<Self::Item> { |
| for line in self.lines.by_ref() { |
| self.line_no += 1; |
| match line.first() { |
| None => continue, |
| Some(b) if *b == b'#' => continue, |
| Some(_) => {} |
| } |
| let line = line.trim(); |
| if line.is_empty() { |
| continue; |
| } |
| return parse_line(line.into(), self.line_no).into(); |
| } |
| None |
| } |
| } |
| |
| fn parse_line(line: &BStr, line_number: usize) -> Result<Entry<'_>, Error> { |
| let (name1, email1, rest) = parse_name_and_email(line, line_number)?; |
| let (name2, email2, rest) = parse_name_and_email(rest, line_number)?; |
| if !rest.trim().is_empty() { |
| return Err(Error::UnconsumedInput { |
| line_number, |
| line: line.into(), |
| }); |
| } |
| Ok(match (name1, email1, name2, email2) { |
| (Some(proper_name), Some(commit_email), None, None) => Entry::change_name_by_email(proper_name, commit_email), |
| (None, Some(proper_email), None, Some(commit_email)) => { |
| Entry::change_email_by_email(proper_email, commit_email) |
| } |
| (Some(proper_name), Some(proper_email), None, Some(commit_email)) => { |
| Entry::change_name_and_email_by_email(proper_name, proper_email, commit_email) |
| } |
| (Some(proper_name), Some(proper_email), Some(commit_name), Some(commit_email)) => { |
| Entry::change_name_and_email_by_name_and_email(proper_name, proper_email, commit_name, commit_email) |
| } |
| _ => { |
| return Err(Error::Malformed { |
| line_number, |
| line: line.into(), |
| message: "Emails without a name or email to map to are invalid".into(), |
| }) |
| } |
| }) |
| } |
| |
| fn parse_name_and_email( |
| line: &BStr, |
| line_number: usize, |
| ) -> Result<(Option<&'_ BStr>, Option<&'_ BStr>, &'_ BStr), Error> { |
| match line.find_byte(b'<') { |
| Some(start_bracket) => { |
| let email = &line[start_bracket + 1..]; |
| let closing_bracket = email.find_byte(b'>').ok_or_else(|| Error::Malformed { |
| line_number, |
| line: line.into(), |
| message: "Missing closing bracket '>' in email".into(), |
| })?; |
| let email = email[..closing_bracket].trim().as_bstr(); |
| if email.is_empty() { |
| return Err(Error::Malformed { |
| line_number, |
| line: line.into(), |
| message: "Email must not be empty".into(), |
| }); |
| } |
| let name = line[..start_bracket].trim().as_bstr(); |
| let rest = line[start_bracket + closing_bracket + 2..].as_bstr(); |
| Ok(((!name.is_empty()).then_some(name), Some(email), rest)) |
| } |
| None => Ok((None, None, line)), |
| } |
| } |