use super::{Anchor, ByteSet, Finder, RunClass, RunRule};
use std::ops::Range;
#[derive(Default)]
pub struct Mac {}
impl Mac {
fn is_hex(b: u8) -> bool {
b.is_ascii_hexdigit()
}
fn try_mac_colon(input: &[u8], idx: usize) -> Option<Range<usize>> {
Self::try_mac_separated(input, idx, b':')
}
fn try_mac_dash(input: &[u8], idx: usize) -> Option<Range<usize>> {
Self::try_mac_separated(input, idx, b'-')
}
fn try_mac_separated(input: &[u8], idx: usize, sep: u8) -> Option<Range<usize>> {
if idx + 17 > input.len() {
return None;
}
for group in 0..6 {
let pos = idx + group * 3;
if group > 0 && input[pos - 1] != sep {
return None;
}
if !Self::is_hex(input[pos]) || !Self::is_hex(input[pos + 1]) {
return None;
}
}
let end = idx + 17;
if end < input.len() && (Self::is_hex(input[end]) || input[end] == sep) {
return None;
}
Some(idx..end)
}
fn try_mac_dot(input: &[u8], idx: usize) -> Option<Range<usize>> {
if idx + 14 > input.len() {
return None;
}
for group in 0..3 {
let pos = idx + group * 5;
if group > 0 && input[pos - 1] != b'.' {
return None;
}
for i in 0..4 {
if !Self::is_hex(input[pos + i]) {
return None;
}
}
}
let end = idx + 14;
if end < input.len() {
if Self::is_hex(input[end]) {
return None;
}
if input[end] == b'.' && end + 1 < input.len() && Self::is_hex(input[end + 1]) {
return None;
}
}
Some(idx..end)
}
}
impl Finder for Mac {
fn line_agnostic(&self) -> bool {
true
}
fn id(&self) -> &'static str {
"mac"
}
fn dispatchable(&self) -> bool {
true
}
fn could_start_at(&self, byte: u8) -> bool {
byte.is_ascii_hexdigit()
}
fn could_start_after(&self, prev: u8, _cur: u8) -> bool {
!(Self::is_hex(prev) || matches!(prev, b':' | b'-' | b'.'))
}
fn could_continue_with(&self, _cur: u8, next: u8) -> bool {
Self::is_hex(next)
}
fn anchor(&self) -> Option<Anchor> {
Some(
Anchor::new(
ByteSet::from_bytes(b":-."),
ByteSet::from_fn(|b| b.is_ascii_hexdigit()),
)
.confirm_all(b":-", &[3, 6, 9, 12], b":-")
.confirm_all(b".", &[5], b"."),
)
}
fn run_rules(&self) -> Vec<RunRule> {
vec![
RunRule::new(RunClass::Hex, 2, 2)
.followed_by(b":-")
.then(RunClass::Hex, 2, 2)
.followed_by(b":-")
.then(RunClass::Hex, 2, 2)
.followed_by(b":-"),
RunRule::new(RunClass::Hex, 4, 4)
.followed_by(b".")
.then(RunClass::Hex, 4, 4)
.followed_by(b".")
.then(RunClass::Hex, 4, 4),
]
}
fn try_at(&self, input: &[u8], pos: usize) -> Option<Range<usize>> {
if !Self::is_hex(input[pos]) {
return None;
}
if pos > 0
&& (Self::is_hex(input[pos - 1])
|| input[pos - 1] == b':'
|| input[pos - 1] == b'-'
|| input[pos - 1] == b'.')
{
return None;
}
Self::try_mac_colon(input, pos)
.or_else(|| Self::try_mac_dash(input, pos))
.or_else(|| Self::try_mac_dot(input, pos))
}
fn find(&self, s: &str) -> Option<Range<usize>> {
let input = s.as_bytes();
let mut idx = 0;
while idx < input.len() {
if Self::is_hex(input[idx]) {
if idx > 0
&& (Self::is_hex(input[idx - 1])
|| input[idx - 1] == b':'
|| input[idx - 1] == b'-'
|| input[idx - 1] == b'.')
{
idx += 1;
continue;
}
if let Some(range) = Self::try_mac_colon(input, idx) {
return Some(range);
}
if let Some(range) = Self::try_mac_dash(input, idx) {
return Some(range);
}
if let Some(range) = Self::try_mac_dot(input, idx) {
return Some(range);
}
}
idx += 1;
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn id_should_return_mac() {
let finder = Mac::default();
assert_eq!("mac", finder.id());
}
#[test]
fn find_should_extract_colon_separated() {
let finder = Mac::default();
let input = "interface: 00:1A:2B:3C:4D:5E";
let range = finder.find(input).unwrap();
assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
}
#[test]
fn find_should_extract_dash_separated() {
let finder = Mac::default();
let input = "interface: 00-1A-2B-3C-4D-5E";
let range = finder.find(input).unwrap();
assert_eq!("00-1A-2B-3C-4D-5E", &input[range]);
}
#[test]
fn find_should_extract_dot_separated() {
let finder = Mac::default();
let input = "interface: 001A.2B3C.4D5E";
let range = finder.find(input).unwrap();
assert_eq!("001A.2B3C.4D5E", &input[range]);
}
#[test]
fn find_should_extract_lowercase() {
let finder = Mac::default();
let input = "aa:bb:cc:dd:ee:ff";
let range = finder.find(input).unwrap();
assert_eq!("aa:bb:cc:dd:ee:ff", &input[range]);
}
#[test]
fn find_should_extract_uppercase() {
let finder = Mac::default();
let input = "AA:BB:CC:DD:EE:FF";
let range = finder.find(input).unwrap();
assert_eq!("AA:BB:CC:DD:EE:FF", &input[range]);
}
#[test]
fn find_should_extract_mac_in_text() {
let finder = Mac::default();
let input = "device 00:1A:2B:3C:4D:5E connected";
let range = finder.find(input).unwrap();
assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
}
#[test]
fn find_should_extract_mac_at_start() {
let finder = Mac::default();
let input = "00:1A:2B:3C:4D:5E is the address";
let range = finder.find(input).unwrap();
assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
}
#[test]
fn find_should_reject_too_short() {
let finder = Mac::default();
assert!(finder.find("00:1A:2B:3C:4D").is_none());
}
#[test]
fn find_should_reject_non_hex() {
let finder = Mac::default();
assert!(finder.find("00:1A:2B:3C:4D:5G").is_none());
}
#[test]
fn find_should_reject_mixed_separators() {
let finder = Mac::default();
assert!(finder.find("00:1A-2B:3C-4D:5E").is_none());
}
#[test]
fn find_should_not_match_within_longer_hex() {
let finder = Mac::default();
assert!(finder.find("ff00:1A:2B:3C:4D:5E").is_none());
}
#[test]
fn find_should_not_match_with_trailing_hex() {
let finder = Mac::default();
assert!(finder.find("00:1A:2B:3C:4D:5Eff").is_none());
}
#[test]
fn find_should_handle_empty_input() {
let finder = Mac::default();
assert!(finder.find("").is_none());
}
#[test]
fn find_should_extract_multiple_macs_iteratively() {
let finder = Mac::default();
let input = "00:1A:2B:3C:4D:5E and AA:BB:CC:DD:EE:FF";
let mut results = Vec::new();
let mut idx = 0;
while idx < input.len() {
if let Some(range) = finder.find(&input[idx..]) {
results.push(&input[idx + range.start..idx + range.end]);
idx += range.end;
} else {
break;
}
}
assert_eq!(vec!["00:1A:2B:3C:4D:5E", "AA:BB:CC:DD:EE:FF"], results);
}
#[test]
fn find_should_extract_broadcast_mac() {
let finder = Mac::default();
let input = "FF:FF:FF:FF:FF:FF";
let range = finder.find(input).unwrap();
assert_eq!("FF:FF:FF:FF:FF:FF", &input[range]);
}
#[test]
fn find_should_extract_zero_mac() {
let finder = Mac::default();
let input = "00:00:00:00:00:00";
let range = finder.find(input).unwrap();
assert_eq!("00:00:00:00:00:00", &input[range]);
}
#[test]
fn try_at_colon_mac() {
let finder = Mac::default();
let input = b"00:1A:2B:3C:4D:5E rest";
assert_eq!(finder.try_at(input, 0), Some(0..17));
}
#[test]
fn try_at_dash_mac() {
let finder = Mac::default();
let input = b"00-1A-2B-3C-4D-5E rest";
assert_eq!(finder.try_at(input, 0), Some(0..17));
}
#[test]
fn try_at_dot_mac() {
let finder = Mac::default();
let input = b"001A.2B3C.4D5E rest";
assert_eq!(finder.try_at(input, 0), Some(0..14));
}
#[test]
fn try_at_preceded_by_hex() {
let finder = Mac::default();
let input = b"ff00:1A:2B:3C:4D:5E";
assert!(finder.try_at(input, 2).is_none());
}
#[test]
fn try_at_preceded_by_colon() {
let finder = Mac::default();
let input = b":00:1A:2B:3C:4D:5E";
assert!(finder.try_at(input, 1).is_none());
}
#[test]
fn try_at_short_input() {
let finder = Mac::default();
let input = b"00:1A";
assert!(finder.try_at(input, 0).is_none());
}
#[test]
fn try_at_non_hex() {
let finder = Mac::default();
let input = b"zz";
assert!(finder.try_at(input, 0).is_none());
}
#[test]
fn find_should_extract_mac_at_end() {
let finder = Mac::default();
let input = "addr 00:1A:2B:3C:4D:5E";
let range = finder.find(input).unwrap();
assert_eq!("00:1A:2B:3C:4D:5E", &input[range]);
}
}