source2rosetta/src/sig.rs
Kamal Tufekcic a2922b8bad
All checks were successful
CI / fuzz (push) Successful in 1m41s
CI / lint (push) Successful in 16s
CI / test (push) Successful in 22s
initial commit
2026-07-27 10:12:04 +03:00

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());
}
}