87 lines
2.7 KiB
Rust
87 lines
2.7 KiB
Rust
//! 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<Option<u8>>,
|
|
}
|
|
|
|
impl Pattern {
|
|
pub fn parse(s: &str) -> anyhow::Result<Self> {
|
|
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<usize> {
|
|
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());
|
|
}
|
|
}
|