1use std::fmt;
2use std::pin::Pin;
3use std::ptr::NonNull;
4
5#[cfg(test)]
6mod test;
7
8pub struct Mailmap {
21 buffer: Pin<Box<str>>,
22 entries: Vec<RawMapEntry>,
23}
24
25impl fmt::Debug for Mailmap {
26 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27 let mut list = f.debug_list();
28 for entry in &self.entries {
29 let entry = unsafe { entry.to_entry(&self.buffer) };
31 list.entry(&entry);
32 }
33 list.finish()
34 }
35}
36
37#[derive(Copy, Clone)]
38struct RawMapEntry {
39 canonical_name: Option<NonNull<str>>,
40 canonical_email: Option<NonNull<str>>,
41 current_name: Option<NonNull<str>>,
42 current_email: Option<NonNull<str>>,
43}
44
45impl RawMapEntry {
46 unsafe fn to_entry<'a>(self, _: &'a str) -> MapEntry<'a> {
47 MapEntry {
48 canonical_name: self.canonical_name.map(|v| &*v.as_ptr()),
49 canonical_email: self.canonical_email.map(|v| &*v.as_ptr()),
50 current_name: self.current_name.map(|v| &*v.as_ptr()),
51 current_email: self.current_email.map(|v| &*v.as_ptr()),
52 }
53 }
54}
55
56#[derive(Copy, Clone, Debug, PartialEq, Eq)]
57struct MapEntry<'a> {
58 canonical_name: Option<&'a str>,
59 canonical_email: Option<&'a str>,
60 current_name: Option<&'a str>,
61 current_email: Option<&'a str>,
62}
63
64impl<'a> MapEntry<'a> {
65 fn to_raw_entry(self) -> RawMapEntry {
66 RawMapEntry {
67 canonical_name: self.canonical_name.map(|v| v.into()),
68 canonical_email: self.canonical_email.map(|v| v.into()),
69 current_name: self.current_name.map(|v| v.into()),
70 current_email: self.current_email.map(|v| v.into()),
71 }
72 }
73}
74
75#[derive(Clone, PartialEq, PartialOrd, Ord, Eq, Hash)]
76pub struct Author {
77 pub name: String,
78 pub email: String,
79}
80
81impl fmt::Debug for Author {
82 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
83 write!(f, "{} <{}>", self.name, self.email)
84 }
85}
86
87impl Mailmap {
88 pub fn from_string(file: String) -> Result<Mailmap, Box<dyn std::error::Error>> {
89 let file = Pin::new(file.into_boxed_str());
90 let mut entries = Vec::with_capacity(file.lines().count());
91 for (idx, line) in file.lines().enumerate() {
92 if let Some(entry) = parse_line(&line, idx + 1) {
93 entries.push(entry.to_raw_entry());
94 }
95 }
96 Ok(Mailmap {
97 buffer: file,
98 entries,
99 })
100 }
101
102 pub fn canonicalize(&self, author: &Author) -> Author {
103 for entry in &self.entries {
104 let entry = unsafe { entry.to_entry(&self.buffer) };
106 if let Some(email) = entry.current_email {
107 if let Some(name) = entry.current_name {
108 if author.name == name && author.email == email {
109 return Author {
110 name: entry.canonical_name.unwrap_or(&author.name).to_owned(),
111 email: entry.canonical_email.expect("canonical email").to_owned(),
112 };
113 }
114 } else {
115 if author.email == email {
116 return Author {
117 name: entry.canonical_name.unwrap_or(&author.name).to_owned(),
118 email: entry.canonical_email.expect("canonical email").to_owned(),
119 };
120 }
121 }
122 }
123 }
124
125 author.clone()
126 }
127}
128
129fn read_email<'a>(line: &mut &'a str) -> Option<&'a str> {
130 if !line.starts_with('<') {
131 return None;
132 }
133
134 let end = line
135 .find('>')
136 .unwrap_or_else(|| panic!("could not find email end in {:?}", line));
137 let ret = &line[1..end];
138 *line = &line[end + 1..];
139 Some(ret)
140}
141
142fn read_name<'a>(line: &mut &'a str) -> Option<&'a str> {
143 let end = if let Some(end) = line.find('<') {
144 end
145 } else {
146 return None;
147 };
148 let ret = &line[..end].trim();
149 *line = &line[end..];
150 if ret.is_empty() {
151 None
152 } else {
153 Some(ret)
154 }
155}
156
157fn read_comment(line: &mut &str) -> bool {
158 if line.trim().starts_with('#') {
159 *line = "";
160 true
161 } else {
162 false
163 }
164}
165
166fn parse_line(mut line: &str, idx: usize) -> Option<MapEntry<'_>> {
167 let mut entry = MapEntry {
168 canonical_name: None,
169 canonical_email: None,
170 current_name: None,
171 current_email: None,
172 };
173 loop {
174 line = line.trim_start();
175 if read_comment(&mut line) || line.trim().is_empty() {
176 break;
177 }
178
179 if let Some(email) = read_email(&mut line) {
180 if entry.canonical_email.is_none() {
181 entry.canonical_email = Some(email);
182 } else {
183 if entry.current_email.is_some() {
184 eprintln!("malformed mailmap on line {}: too many emails", idx);
185 } else {
186 entry.current_email = Some(email);
187 }
188 }
189 } else if let Some(name) = read_name(&mut line) {
190 if entry.canonical_name.is_none() {
191 entry.canonical_name = Some(name);
192 } else {
193 if entry.current_name.is_some() {
194 eprintln!("malformed mailmap on line {}: too many names", idx);
195 } else {
196 entry.current_name = Some(name);
197 }
198 }
199 } else {
200 break;
201 }
202 }
203
204 if entry.canonical_email.is_some() && entry.current_email.is_none() {
205 entry.current_email = entry.canonical_email;
206 }
207
208 if entry.canonical_name.is_some()
209 || entry.canonical_email.is_some()
210 || entry.current_name.is_some()
211 || entry.current_email.is_some()
212 {
213 Some(entry)
214 } else {
215 None
216 }
217}