1use super::{Anchor, ByteSet, Finder, RunClass, RunRule};
2use std::ops::Range;
3
4#[derive(Default)]
5pub struct Mac {}
6
7impl Mac {
8 fn is_hex(b: u8) -> bool {
9 b.is_ascii_hexdigit()
10 }
11
12 fn try_mac_colon(input: &[u8], idx: usize) -> Option<Range<usize>> {
13 Self::try_mac_separated(input, idx, b':')
15 }
16
17 fn try_mac_dash(input: &[u8], idx: usize) -> Option<Range<usize>> {
18 Self::try_mac_separated(input, idx, b'-')
20 }
21
22 fn try_mac_separated(input: &[u8], idx: usize, sep: u8) -> Option<Range<usize>> {
23 if idx + 17 > input.len() {
24 return None;
25 }
26
27 for group in 0..6 {
28 let pos = idx + group * 3;
29 if group > 0 && input[pos - 1] != sep {
30 return None;
31 }
32 if !Self::is_hex(input[pos]) || !Self::is_hex(input[pos + 1]) {
33 return None;
34 }
35 }
36
37 let end = idx + 17;
38
39 if end < input.len() && (Self::is_hex(input[end]) || input[end] == sep) {
41 return None;
42 }
43
44 Some(idx..end)
45 }
46
47 fn try_mac_dot(input: &[u8], idx: usize) -> Option<Range<usize>> {
48 if idx + 14 > input.len() {
50 return None;
51 }
52
53 for group in 0..3 {
54 let pos = idx + group * 5;
55 if group > 0 && input[pos - 1] != b'.' {
56 return None;
57 }
58 for i in 0..4 {
59 if !Self::is_hex(input[pos + i]) {
60 return None;
61 }
62 }
63 }
64
65 let end = idx + 14;
66
67 if end < input.len() {
71 if Self::is_hex(input[end]) {
72 return None;
73 }
74 if input[end] == b'.' && end + 1 < input.len() && Self::is_hex(input[end + 1]) {
75 return None;
76 }
77 }
78
79 Some(idx..end)
80 }
81}
82
83impl Finder for Mac {
84 fn line_agnostic(&self) -> bool {
85 true
88 }
89
90 fn id(&self) -> &'static str {
91 "mac"
92 }
93
94 fn dispatchable(&self) -> bool {
95 true
96 }
97
98 fn could_start_at(&self, byte: u8) -> bool {
99 byte.is_ascii_hexdigit()
100 }
101
102 fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
103 !(Self::is_hex(prev) || matches!(prev, b':' | b'-' | b'.'))
104 }
105
106 fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
107 Self::is_hex(next)
108 }
109
110 fn anchor(&self) -> Option<Anchor> {
111 Some(
115 Anchor::new(
116 ByteSet::from_bytes(b":-."),
117 ByteSet::from_fn(|b| b.is_ascii_hexdigit()),
118 )
119 .confirm_all(b":-", &[3, 6, 9, 12], b":-")
120 .confirm_all(b".", &[5], b"."),
121 )
122 }
123
124 fn run_rules(&self) -> Vec<RunRule> {
125 vec![
127 RunRule::new(RunClass::Hex, 2, 2)
128 .followed_by(b":-")
129 .then(RunClass::Hex, 2, 2)
130 .followed_by(b":-")
131 .then(RunClass::Hex, 2, 2)
132 .followed_by(b":-"),
133 RunRule::new(RunClass::Hex, 4, 4)
134 .followed_by(b".")
135 .then(RunClass::Hex, 4, 4)
136 .followed_by(b".")
137 .then(RunClass::Hex, 4, 4),
138 ]
139 }
140
141 fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
142 if !Self::is_hex(input[pos]) {
143 return None;
144 }
145 if pos > 0
146 && (Self::is_hex(input[pos - 1])
147 || input[pos - 1] == b':'
148 || input[pos - 1] == b'-'
149 || input[pos - 1] == b'.')
150 {
151 return None;
152 }
153 Self::try_mac_colon(input, pos)
154 .or_else(|| Self::try_mac_dash(input, pos))
155 .or_else(|| Self::try_mac_dot(input, pos))
156 }
157
158 fn find(&self, s: &str) -> Option<Range<usize>> {
159 let input = s.as_bytes();
160 let mut idx = 0;
161
162 while idx < input.len() {
163 if Self::is_hex(input[idx]) {
164 if idx > 0
166 && (Self::is_hex(input[idx - 1])
167 || input[idx - 1] == b':'
168 || input[idx - 1] == b'-'
169 || input[idx - 1] == b'.')
170 {
171 idx += 1;
172 continue;
173 }
174
175 if let Some(range) = Self::try_mac_colon(input, idx) {
176 return Some(range);
177 }
178 if let Some(range) = Self::try_mac_dash(input, idx) {
179 return Some(range);
180 }
181 if let Some(range) = Self::try_mac_dot(input, idx) {
182 return Some(range);
183 }
184 }
185 idx += 1;
186 }
187
188 None
189 }
190}
191
192#[cfg(test)]
193mod tests {
194 use super::*;
195
196 #[test]
197 fn id_should_return_mac() {
198 let finder = Mac::default();
199 assert_eq!("mac", finder.id());
200 }
201
202 #[test]
203 fn find_should_extract_colon_separated() {
204 let finder = Mac::default();
205 let input = "interface: 00:1A:2B:3C:4D:5E";
206 let range = finder.find(input).unwrap();
207 assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
208 }
209
210 #[test]
211 fn find_should_extract_dash_separated() {
212 let finder = Mac::default();
213 let input = "interface: 00-1A-2B-3C-4D-5E";
214 let range = finder.find(input).unwrap();
215 assert_eq!("00-1A-2B-3C-4D-5E", &input[range]);
216 }
217
218 #[test]
219 fn find_should_extract_dot_separated() {
220 let finder = Mac::default();
221 let input = "interface: 001A.2B3C.4D5E";
222 let range = finder.find(input).unwrap();
223 assert_eq!("001A.2B3C.4D5E", &input[range]);
224 }
225
226 #[test]
227 fn find_should_extract_lowercase() {
228 let finder = Mac::default();
229 let input = "aa:bb:cc:dd:ee:ff";
230 let range = finder.find(input).unwrap();
231 assert_eq!("aa:bb:cc:dd:ee:ff", &input[range]);
232 }
233
234 #[test]
235 fn find_should_extract_uppercase() {
236 let finder = Mac::default();
237 let input = "AA:BB:CC:DD:EE:FF";
238 let range = finder.find(input).unwrap();
239 assert_eq!("AA:BB:CC:DD:EE:FF", &input[range]);
240 }
241
242 #[test]
243 fn find_should_extract_mac_in_text() {
244 let finder = Mac::default();
245 let input = "device 00:1A:2B:3C:4D:5E connected";
246 let range = finder.find(input).unwrap();
247 assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
248 }
249
250 #[test]
251 fn find_should_extract_mac_at_start() {
252 let finder = Mac::default();
253 let input = "00:1A:2B:3C:4D:5E is the address";
254 let range = finder.find(input).unwrap();
255 assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
256 }
257
258 #[test]
259 fn find_should_reject_too_short() {
260 let finder = Mac::default();
261 assert!(finder.find("00:1A:2B:3C:4D").is_none());
262 }
263
264 #[test]
265 fn find_should_reject_non_hex() {
266 let finder = Mac::default();
267 assert!(finder.find("00:1A:2B:3C:4D:5G").is_none());
268 }
269
270 #[test]
271 fn find_should_reject_mixed_separators() {
272 let finder = Mac::default();
273 assert!(finder.find("00:1A-2B:3C-4D:5E").is_none());
274 }
275
276 #[test]
277 fn find_should_not_match_within_longer_hex() {
278 let finder = Mac::default();
279 assert!(finder.find("ff00:1A:2B:3C:4D:5E").is_none());
280 }
281
282 #[test]
283 fn find_should_not_match_with_trailing_hex() {
284 let finder = Mac::default();
285 assert!(finder.find("00:1A:2B:3C:4D:5Eff").is_none());
286 }
287
288 #[test]
289 fn find_should_handle_empty_input() {
290 let finder = Mac::default();
291 assert!(finder.find("").is_none());
292 }
293
294 #[test]
295 fn find_should_extract_multiple_macs_iteratively() {
296 let finder = Mac::default();
297 let input = "00:1A:2B:3C:4D:5E and AA:BB:CC:DD:EE:FF";
298
299 let mut results = Vec::new();
300 let mut idx = 0;
301 while idx < input.len() {
302 if let Some(range) = finder.find(&input[idx..]) {
303 results.push(&input[idx + range.start..idx + range.end]);
304 idx += range.end;
305 } else {
306 break;
307 }
308 }
309
310 assert_eq!(vec!["00:1A:2B:3C:4D:5E", "AA:BB:CC:DD:EE:FF"], results);
311 }
312
313 #[test]
314 fn find_should_extract_broadcast_mac() {
315 let finder = Mac::default();
316 let input = "FF:FF:FF:FF:FF:FF";
317 let range = finder.find(input).unwrap();
318 assert_eq!("FF:FF:FF:FF:FF:FF", &input[range]);
319 }
320
321 #[test]
322 fn find_should_extract_zero_mac() {
323 let finder = Mac::default();
324 let input = "00:00:00:00:00:00";
325 let range = finder.find(input).unwrap();
326 assert_eq!("00:00:00:00:00:00", &input[range]);
327 }
328
329 #[test]
330 fn try_at_colon_mac() {
331 let finder = Mac::default();
332 let input = b"00:1A:2B:3C:4D:5E rest";
333 assert_eq!(finder.try_at(input, 0), Some(0..17));
334 }
335
336 #[test]
337 fn try_at_dash_mac() {
338 let finder = Mac::default();
339 let input = b"00-1A-2B-3C-4D-5E rest";
340 assert_eq!(finder.try_at(input, 0), Some(0..17));
341 }
342
343 #[test]
344 fn try_at_dot_mac() {
345 let finder = Mac::default();
346 let input = b"001A.2B3C.4D5E rest";
347 assert_eq!(finder.try_at(input, 0), Some(0..14));
348 }
349
350 #[test]
351 fn try_at_preceded_by_hex() {
352 let finder = Mac::default();
353 let input = b"ff00:1A:2B:3C:4D:5E";
354 assert!(finder.try_at(input, 2).is_none());
355 }
356
357 #[test]
358 fn try_at_preceded_by_colon() {
359 let finder = Mac::default();
360 let input = b":00:1A:2B:3C:4D:5E";
361 assert!(finder.try_at(input, 1).is_none());
362 }
363
364 #[test]
365 fn try_at_short_input() {
366 let finder = Mac::default();
367 let input = b"00:1A";
368 assert!(finder.try_at(input, 0).is_none());
369 }
370
371 #[test]
372 fn try_at_non_hex() {
373 let finder = Mac::default();
374 let input = b"zz";
375 assert!(finder.try_at(input, 0).is_none());
376 }
377
378 #[test]
379 fn find_should_extract_mac_at_end() {
380 let finder = Mac::default();
381 let input = "addr 00:1A:2B:3C:4D:5E";
382 let range = finder.find(input).unwrap();
383 assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
384 }
385}