use aho_corasick::{AhoCorasick, AhoCorasickKind};
use rustc_hash::FxHashMap;
use super::{FindError, Pattern};
const MIN_CHUNK: usize = 1 << 20;
#[must_use]
pub fn find_each_unique(patterns: &[&Pattern], code: &[u8]) -> Vec<Result<usize, FindError>> {
let threads = std::thread::available_parallelism().map_or(1, usize::from);
find_on(patterns, code, threads, MIN_CHUNK)
}
fn find_on(
patterns: &[&Pattern],
code: &[u8],
threads: usize,
min_chunk: usize,
) -> Vec<Result<usize, FindError>> {
let Some(anchors) = Anchors::new(patterns) else {
return patterns.iter().map(|p| p.find_unique(code)).collect();
};
let chunk = code
.len()
.div_ceil(threads.clamp(1, 16))
.max(min_chunk.max(1));
let tallies: Option<Vec<Vec<Seen>>> = std::thread::scope(|scope| {
let workers: Vec<_> = (0..code.len().div_ceil(chunk).max(1))
.map(|i| {
let anchors = &anchors;
let from = i * chunk;
let to = from.saturating_add(chunk).min(code.len());
scope.spawn(move || anchors.tally(code, from, to))
})
.collect();
workers
.into_iter()
.map(|worker| worker.join().ok().flatten())
.collect()
});
let Some(tallies) = tallies else {
return patterns.iter().map(|p| p.find_unique(code)).collect();
};
let mut seen = vec![Seen::None; patterns.len()];
for tally in tallies {
for (total, part) in seen.iter_mut().zip(tally) {
*total = total.and(part);
}
}
seen.into_iter().map(Seen::unique).collect()
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Seen {
None,
Once(usize),
Many,
}
impl Seen {
const fn and(self, other: Self) -> Self {
match (self, other) {
(Self::None, seen) | (seen, Self::None) => seen,
_ => Self::Many,
}
}
const fn unique(self) -> Result<usize, FindError> {
match self {
Self::None => Err(FindError::None),
Self::Once(at) => Ok(at),
Self::Many => Err(FindError::Many),
}
}
}
struct Anchors<'p> {
automaton: AhoCorasick,
users: Vec<Vec<usize>>,
patterns: &'p [&'p Pattern],
longest: usize,
}
impl<'p> Anchors<'p> {
fn new(patterns: &'p [&'p Pattern]) -> Option<Self> {
let mut index: FxHashMap<Vec<u8>, usize> = FxHashMap::default();
let mut needles: Vec<Vec<u8>> = Vec::new();
let mut users: Vec<Vec<usize>> = Vec::new();
for (i, pattern) in patterns.iter().enumerate() {
let needle: Vec<u8> = pattern.bytes[pattern.anchor.clone()]
.iter()
.map(|b| b.unwrap_or(0))
.collect();
let anchor = *index.entry(needle.clone()).or_insert_with(|| {
needles.push(needle);
users.push(Vec::new());
users.len() - 1
});
users.get_mut(anchor)?.push(i);
}
let longest = needles.iter().map(Vec::len).max().unwrap_or(0);
let automaton = AhoCorasick::builder()
.kind(Some(AhoCorasickKind::DFA))
.build(&needles)
.ok()?;
Some(Self {
automaton,
users,
patterns,
longest,
})
}
fn tally(&self, code: &[u8], from: usize, to: usize) -> Option<Vec<Seen>> {
let mut seen = vec![Seen::None; self.patterns.len()];
let end = to.saturating_add(self.longest).min(code.len());
let hay = code.get(from..end)?;
for hit in self.automaton.try_find_overlapping_iter(hay).ok()? {
let at = from + hit.start();
if at >= to {
continue;
}
for &i in self.users.get(hit.pattern().as_usize())? {
let pattern = self.patterns.get(i)?;
let Some(start) = at.checked_sub(pattern.anchor.start) else {
continue;
};
if code.get(start..).is_some_and(|rest| pattern.matches(rest)) {
let slot = seen.get_mut(i)?;
*slot = slot.and(Seen::Once(start));
}
}
}
Some(seen)
}
}
#[cfg(test)]
mod tests {
use proptest::prelude::*;
use super::*;
fn each(
patterns: &[Pattern],
code: &[u8],
threads: usize,
min_chunk: usize,
) -> Vec<Result<usize, FindError>> {
let refs: Vec<&Pattern> = patterns.iter().collect();
find_on(&refs, code, threads, min_chunk)
}
fn one_by_one(patterns: &[Pattern], code: &[u8]) -> Vec<Result<usize, FindError>> {
patterns.iter().map(|p| p.find_unique(code)).collect()
}
#[test]
fn patterns_sharing_an_anchor_are_each_checked_in_full() {
let patterns = [
Pattern::parse("48 8B 05 ?? ?? ?? ?? C3").unwrap(),
Pattern::parse("48 8B 05 ?? ?? ?? ?? 90").unwrap(),
Pattern::parse("?? 48 8B 05").unwrap(),
];
let code = [
0xcc, 0x48, 0x8b, 0x05, 1, 2, 3, 4, 0xc3, 0x48, 0x8b, 0x05, 0, 0, 0, 0, 0x90,
];
assert_eq!(
each(&patterns, &code, 1, 1),
[Ok(1), Ok(9), Err(FindError::Many)]
);
}
#[test]
fn a_match_across_a_part_boundary_counts_once() {
let patterns = [Pattern::parse("11 22 33 44 ?? 55").unwrap()];
let mut code = vec![0u8; 64];
code[30..36].copy_from_slice(&[0x11, 0x22, 0x33, 0x44, 0, 0x55]);
for chunk in 1..code.len() {
assert_eq!(
each(&patterns, &code, 16, chunk),
[Ok(30)],
"parts of {chunk}"
);
}
}
#[test]
fn an_anchor_before_its_lead_bytes_is_no_match() {
let patterns = [Pattern::parse("?? ?? C3 C3").unwrap()];
assert_eq!(
each(&patterns, &[0xc3, 0xc3, 0x90], 1, 1),
[Err(FindError::None)]
);
}
#[test]
fn no_patterns_and_no_code_find_nothing() {
assert!(each(&[], &[1, 2, 3], 4, 1).is_empty());
let patterns = [Pattern::parse("C3").unwrap()];
assert_eq!(each(&patterns, &[], 4, 1), [Err(FindError::None)]);
}
fn pattern() -> impl Strategy<Value = Pattern> {
prop::collection::vec(prop::option::weighted(0.8, 0u8..4), 1..6).prop_filter_map(
"a fixed byte",
|bytes| {
let text: Vec<String> = bytes
.iter()
.map(|b| b.map_or_else(|| "??".to_owned(), |b| format!("{b:02x}")))
.collect();
Pattern::parse(&text.join(" ")).ok()
},
)
}
proptest! {
#[test]
fn one_pass_finds_what_each_pattern_finds_alone(
patterns in prop::collection::vec(pattern(), 0..12),
code in prop::collection::vec(0u8..4, 0..300),
threads in 1usize..6,
chunk in 1usize..80,
) {
prop_assert_eq!(each(&patterns, &code, threads, chunk), one_by_one(&patterns, &code));
}
}
}