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