//! Byte-pattern signatures: parse "55 48 89 ? E5" and scan a byte haystack for matches. /// A signature pattern; `None` entries are wildcards (`?` / `??`). #[derive(Debug, Clone)] pub struct Pattern { bytes: Vec>, } impl Pattern { pub fn parse(s: &str) -> anyhow::Result { let mut bytes = Vec::new(); for tok in s.split_whitespace() { match tok { "?" | "??" | "*" => bytes.push(None), hex => { let b = u8::from_str_radix(hex, 16) .map_err(|_| anyhow::anyhow!("bad signature token {tok:?}"))?; bytes.push(Some(b)); } } } anyhow::ensure!(!bytes.is_empty(), "empty signature"); anyhow::ensure!(bytes.iter().any(Option::is_some), "all-wildcard signature"); Ok(Self { bytes }) } /// First concrete (non-wildcard) byte and its index — used as a cheap scan prefilter. fn anchor(&self) -> (usize, u8) { self.bytes .iter() .enumerate() .find_map(|(i, b)| b.map(|v| (i, v))) .expect("parse() guarantees at least one concrete byte") } /// Byte offsets in `hay` where this pattern matches. pub fn find_all(&self, hay: &[u8]) -> Vec { let n = self.bytes.len(); let mut out = Vec::new(); if hay.len() < n { return out; } let (ai, av) = self.anchor(); // SIMD-scan for the anchor byte (memchr), full-match only at candidate starts. // A match starting at `pos` puts its anchor at `pos + ai`, so valid anchor indices // are [ai, hay.len()-n+ai]; scanning [..=hi] finds every match's anchor, none missed. let hi = hay.len() - n + ai; for apos in memchr::memchr_iter(av, &hay[..=hi]) { if apos < ai { continue; } let pos = apos - ai; if self .bytes .iter() .enumerate() .all(|(i, b)| b.is_none_or(|v| hay[pos + i] == v)) { out.push(pos); } } out } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_and_match() { let p = Pattern::parse("55 48 ? E5").unwrap(); let hay = [0x00, 0x55, 0x48, 0x99, 0xE5, 0x55, 0x48, 0x11, 0xE5]; assert_eq!(p.find_all(&hay), vec![1, 5]); } #[test] fn double_question_is_wildcard() { let p = Pattern::parse("90 ?? 90").unwrap(); assert_eq!(p.find_all(&[0x90, 0xAB, 0x90]), vec![0]); } #[test] fn all_wildcard_rejected() { assert!(Pattern::parse("? ??").is_err()); } }