use crate::fastscan::{first_byte_in_raw, first_byte_in_slice};
use crate::pattern::WildcardPattern;
use crate::pe::{exec_sections, text_section_bounds};
#[inline]
unsafe fn matches_at(addr: usize, pattern: WildcardPattern<'_>) -> bool {
for (i, slot) in pattern.iter().enumerate() {
if let Some(want) = *slot {
let got = *((addr + i) as *const u8);
if got != want {
return false;
}
}
}
true
}
#[must_use]
pub fn find_in_text(module_base: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
if module_base == 0 || pattern.is_empty() {
return None;
}
let (start, size) = text_section_bounds(module_base)?;
scan_range(start, size, pattern)
}
#[must_use]
pub fn count_in_text(module_base: usize, pattern: WildcardPattern<'_>) -> usize {
if module_base == 0 || pattern.is_empty() {
return 0;
}
match text_section_bounds(module_base) {
Some((start, size)) => count_range(start, size, pattern),
None => 0,
}
}
#[must_use]
pub fn find_in_exec_sections(module_base: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
if module_base == 0 || pattern.is_empty() {
return None;
}
for (start, size) in exec_sections(module_base)? {
if let Some(addr) = scan_range(start, size, pattern) {
return Some(addr);
}
}
None
}
#[must_use]
pub fn count_in_exec_sections(module_base: usize, pattern: WildcardPattern<'_>) -> usize {
if module_base == 0 || pattern.is_empty() {
return 0;
}
let Some(sections) = exec_sections(module_base) else {
return 0;
};
let mut total = 0usize;
for (start, size) in sections {
total += count_range(start, size, pattern);
}
total
}
#[must_use]
pub fn find_in_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> Option<usize> {
if pattern.is_empty() || haystack.len() < pattern.len() {
return None;
}
scan_slice(haystack, pattern).map(|off| haystack.as_ptr() as usize + off)
}
#[must_use]
pub fn count_in_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> usize {
if pattern.is_empty() || haystack.len() < pattern.len() {
return 0;
}
count_slice(haystack, pattern)
}
#[inline]
fn anchor(pattern: WildcardPattern<'_>) -> Option<(usize, u8)> {
pattern
.iter()
.enumerate()
.find_map(|(i, b)| b.map(|byte| (i, byte)))
}
fn scan_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> Option<usize> {
let pat_len = pattern.len();
let upper = haystack.len() - pat_len; let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
return Some(0);
};
let mut i = 0usize;
while i <= upper {
let search_from = i + anchor_off;
let Some(rel) = first_byte_in_slice(&haystack[search_from..], anchor_byte) else {
return None;
};
let candidate = search_from + rel - anchor_off;
if candidate > upper {
return None;
}
if unsafe { matches_at(haystack.as_ptr() as usize + candidate, pattern) } {
return Some(candidate);
}
i = candidate + 1;
}
None
}
fn count_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> usize {
let pat_len = pattern.len();
let upper = haystack.len() - pat_len;
let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
return haystack.len() / pat_len;
};
let mut count = 0usize;
let mut i = 0usize;
while i <= upper {
let search_from = i + anchor_off;
let Some(rel) = first_byte_in_slice(&haystack[search_from..], anchor_byte) else {
break;
};
let candidate = search_from + rel - anchor_off;
if candidate > upper {
break;
}
if unsafe { matches_at(haystack.as_ptr() as usize + candidate, pattern) } {
count += 1;
i = candidate + pat_len;
} else {
i = candidate + 1;
}
}
count
}
fn scan_range(start: usize, size: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
let pat_len = pattern.len();
if size < pat_len {
return None;
}
let upper = size - pat_len;
let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
return Some(start);
};
let mut i = 0usize;
while i <= upper {
let search_from = i + anchor_off;
let Some(rel) =
(unsafe { first_byte_in_raw(start + search_from, size - search_from, anchor_byte) })
else {
return None;
};
let candidate = search_from + rel - anchor_off;
if candidate > upper {
return None;
}
let addr = start + candidate;
if unsafe { matches_at(addr, pattern) } {
return Some(addr);
}
i = candidate + 1;
}
None
}
fn count_range(start: usize, size: usize, pattern: WildcardPattern<'_>) -> usize {
let pat_len = pattern.len();
if size < pat_len {
return 0;
}
let upper = size - pat_len;
let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
return size / pat_len;
};
let mut count = 0usize;
let mut i = 0usize;
while i <= upper {
let search_from = i + anchor_off;
let Some(rel) =
(unsafe { first_byte_in_raw(start + search_from, size - search_from, anchor_byte) })
else {
break;
};
let candidate = search_from + rel - anchor_off;
if candidate > upper {
break;
}
let addr = start + candidate;
if unsafe { matches_at(addr, pattern) } {
count += 1;
i = candidate + pat_len;
} else {
i = candidate + 1;
}
}
count
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pattern;
use crate::pe::IMAGE_SCN_MEM_EXECUTE;
use alloc::vec;
use alloc::vec::Vec;
fn synthetic_pe(sections: &[([u8; 8], u32, &[u8], u32)]) -> Vec<u8> {
let needed = sections
.iter()
.map(|(_, vaddr, bytes, _)| *vaddr as usize + bytes.len())
.max()
.unwrap_or(0)
.max(0x400);
let mut buf = vec![0u8; needed];
buf[0] = b'M';
buf[1] = b'Z';
let nt_offset: u32 = 0x80;
buf[0x3C..0x40].copy_from_slice(&nt_offset.to_le_bytes());
let nt = nt_offset as usize;
buf[nt..nt + 4].copy_from_slice(b"PE\0\0");
let num_sections: u16 = sections.len() as u16;
buf[nt + 4 + 2..nt + 4 + 4].copy_from_slice(&num_sections.to_le_bytes());
let opt_size: u16 = 0xF0;
buf[nt + 4 + 16..nt + 4 + 18].copy_from_slice(&opt_size.to_le_bytes());
let section_table = nt + 4 + 20 + opt_size as usize;
for (i, (name, vaddr, bytes, characteristics)) in sections.iter().enumerate() {
let sec = section_table + i * 40;
buf[sec..sec + 8].copy_from_slice(name);
let vsize: u32 = bytes.len() as u32;
buf[sec + 8..sec + 12].copy_from_slice(&vsize.to_le_bytes());
buf[sec + 12..sec + 16].copy_from_slice(&vaddr.to_le_bytes());
buf[sec + 36..sec + 40].copy_from_slice(&characteristics.to_le_bytes());
let v = *vaddr as usize;
buf[v..v + bytes.len()].copy_from_slice(bytes);
}
buf
}
#[test]
fn slice_find_basic() {
let haystack = [0x00, 0x11, 0x48, 0x8B, 0x05, 0x99, 0xAA];
let pat = pattern![0x48, 0x8B, 0x05];
let hit = find_in_slice(&haystack, pat).unwrap();
assert_eq!(hit, haystack.as_ptr() as usize + 2);
}
#[test]
fn slice_find_wildcard() {
let haystack = [0x00, 0x48, 0x77, 0x05, 0xFF];
let pat = pattern![0x48, _, 0x05];
let hit = find_in_slice(&haystack, pat).unwrap();
assert_eq!(hit, haystack.as_ptr() as usize + 1);
}
#[test]
fn slice_find_misses_returns_none() {
let haystack = [0x00, 0x11, 0x22];
let pat = pattern![0x48, 0x8B, 0x05];
assert!(find_in_slice(&haystack, pat).is_none());
}
#[test]
fn slice_find_empty_pattern_returns_none() {
let haystack = [0x00, 0x11];
let pat: &[Option<u8>] = &[];
assert!(find_in_slice(&haystack, pat).is_none());
}
#[test]
fn slice_find_pattern_longer_than_haystack() {
let haystack = [0x48];
let pat = pattern![0x48, 0x8B, 0x05];
assert!(find_in_slice(&haystack, pat).is_none());
}
#[test]
fn slice_find_all_wildcards_matches_first() {
let haystack = [0xAA, 0xBB, 0xCC];
let pat: &[Option<u8>] = &[None, None];
let hit = find_in_slice(&haystack, pat).unwrap();
assert_eq!(hit, haystack.as_ptr() as usize);
}
#[test]
fn slice_find_anchor_match_but_full_pattern_mismatch() {
let haystack = [0x48, 0xFF, 0x48, 0x8B];
let pat = pattern![0x48, 0x8B];
let hit = find_in_slice(&haystack, pat).unwrap();
assert_eq!(hit, haystack.as_ptr() as usize + 2);
}
#[test]
fn slice_count_anchor_match_but_full_pattern_mismatch() {
let haystack = [0x48, 0xFF, 0x48, 0x8B];
let pat = pattern![0x48, 0x8B];
assert_eq!(count_in_slice(&haystack, pat), 1);
}
#[test]
fn slice_find_anchor_in_middle() {
let haystack = [0xCC, 0x77, 0x99, 0xAA, 0x77, 0x99];
let pat: &[Option<u8>] = &[None, Some(0x77), Some(0x99)];
let hit = find_in_slice(&haystack, pat).unwrap();
assert_eq!(hit, haystack.as_ptr() as usize);
}
#[test]
fn slice_count_basic() {
let haystack = [0x48, 0x8B, 0x00, 0x48, 0x8B, 0x00, 0x48, 0x8B];
let pat = pattern![0x48, 0x8B];
assert_eq!(count_in_slice(&haystack, pat), 3);
}
#[test]
fn slice_count_no_overlap() {
let haystack = [0x42, 0x42, 0x42, 0x42];
let pat = pattern![0x42, 0x42];
assert_eq!(count_in_slice(&haystack, pat), 2);
}
#[test]
fn slice_count_zero_when_no_match() {
let haystack = [0x00, 0x11, 0x22];
let pat = pattern![0x48];
assert_eq!(count_in_slice(&haystack, pat), 0);
}
#[test]
fn slice_count_empty_pattern_returns_zero() {
let haystack = [0x00, 0x11];
let pat: &[Option<u8>] = &[];
assert_eq!(count_in_slice(&haystack, pat), 0);
}
#[test]
fn slice_count_pattern_longer_than_haystack() {
let haystack = [0x48];
let pat = pattern![0x48, 0x8B, 0x05];
assert_eq!(count_in_slice(&haystack, pat), 0);
}
#[test]
fn slice_count_all_wildcards() {
let haystack = [0xAA, 0xBB, 0xCC, 0xDD];
let pat: &[Option<u8>] = &[None, None];
assert_eq!(count_in_slice(&haystack, pat), 2);
}
#[test]
fn synthetic_pe_text_find_and_count() {
let text = [0x00u8, 0x11, 0x48, 0x8B, 0x05, 0xFF, 0x00, 0x48, 0x8B, 0x05];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &text, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x48, 0x8B, 0x05];
let hit = find_in_text(base, pat).unwrap();
assert_eq!(hit, base + 0x300 + 2);
assert_eq!(count_in_text(base, pat), 2);
}
#[test]
fn synthetic_pe_text_find_returns_none_when_no_match() {
let text = [0xAAu8, 0xBB, 0xCC];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &text, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x48, 0x8B];
assert!(find_in_text(base, pat).is_none());
assert_eq!(count_in_text(base, pat), 0);
}
#[test]
fn synthetic_pe_text_returns_none_when_no_text_section() {
let body = [0x48u8, 0x8B];
let buf = synthetic_pe(&[(*b".data\0\0\0", 0x300, &body, 0)]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x48, 0x8B];
assert!(find_in_text(base, pat).is_none());
assert_eq!(count_in_text(base, pat), 0);
}
#[test]
fn synthetic_pe_exec_sections_find_across_sections() {
let body_a = [0xAAu8, 0xBB];
let body_b = [0x90u8, 0x90, 0xC3];
let buf = synthetic_pe(&[
(*b".text\0\0\0", 0x300, &body_a, IMAGE_SCN_MEM_EXECUTE),
(*b".text$mn", 0x310, &body_b, IMAGE_SCN_MEM_EXECUTE),
]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x90, 0x90, 0xC3];
let hit = find_in_exec_sections(base, pat).unwrap();
assert_eq!(hit, base + 0x310);
assert_eq!(count_in_exec_sections(base, pat), 1);
}
#[test]
fn synthetic_pe_exec_sections_count_sums_across_sections() {
let body = [0x90u8, 0x90];
let buf = synthetic_pe(&[
(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE),
(*b".text$mn", 0x310, &body, IMAGE_SCN_MEM_EXECUTE),
]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x90, 0x90];
assert_eq!(count_in_exec_sections(base, pat), 2);
}
#[test]
fn synthetic_pe_exec_sections_returns_none_when_no_match() {
let body = [0xAAu8];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x48];
assert!(find_in_exec_sections(base, pat).is_none());
assert_eq!(count_in_exec_sections(base, pat), 0);
}
#[test]
fn null_module_returns_none_or_zero() {
let pat = pattern![0x48];
assert!(find_in_text(0, pat).is_none());
assert_eq!(count_in_text(0, pat), 0);
assert!(find_in_exec_sections(0, pat).is_none());
assert_eq!(count_in_exec_sections(0, pat), 0);
}
#[test]
fn empty_pattern_returns_none_or_zero() {
let body = [0x90u8];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat: &[Option<u8>] = &[];
assert!(find_in_text(base, pat).is_none());
assert_eq!(count_in_text(base, pat), 0);
assert!(find_in_exec_sections(base, pat).is_none());
assert_eq!(count_in_exec_sections(base, pat), 0);
}
#[test]
fn malformed_module_returns_none_or_zero() {
let buf = vec![0u8; 0x400];
let base = buf.as_ptr() as usize;
let pat = pattern![0x48];
assert!(find_in_text(base, pat).is_none());
assert_eq!(count_in_text(base, pat), 0);
assert!(find_in_exec_sections(base, pat).is_none());
assert_eq!(count_in_exec_sections(base, pat), 0);
}
#[test]
fn synthetic_pe_text_all_wildcard_pattern() {
let text = [0xAAu8; 16];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &text, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat: &[Option<u8>] = &[None, None, None, None];
let hit = find_in_text(base, pat).unwrap();
let (text_start, _) = crate::pe::text_section_bounds(base).unwrap();
assert_eq!(hit, text_start);
let count = count_in_text(base, pat);
assert!(count >= text.len() / 4);
}
#[test]
fn slice_anchor_at_tail_no_room_for_pattern() {
let mut buf = vec![0u8; 16];
buf[15] = 0x48;
let pat = pattern![0x48, 0x8B, 0x05, _];
assert!(crate::find_in_slice(&buf, pat).is_none());
assert_eq!(crate::count_in_slice(&buf, pat), 0);
}
#[test]
fn slice_find_loop_exhausts_when_last_candidate_fails() {
let haystack = [0x00, 0x00, 0x48, 0xFF];
let pat = pattern![0x48, 0x8B];
assert!(find_in_slice(&haystack, pat).is_none());
}
#[test]
fn synthetic_pe_text_anchor_match_but_full_pattern_mismatch() {
let body = [0x48, 0x00, 0x00, 0x00, 0x48];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x48, 0xAA, 0xBB, 0xCC];
assert!(find_in_text(base, pat).is_none());
assert_eq!(count_in_text(base, pat), 0);
}
#[test]
fn synthetic_pe_text_anchor_at_upper_then_loop_exhausts() {
let body = [0x48, 0x00, 0x00, 0x00];
let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
let base = buf.as_ptr() as usize;
let pat = pattern![0x48, 0xAA, 0xBB, 0xCC];
assert!(find_in_text(base, pat).is_none());
assert_eq!(count_in_text(base, pat), 0);
}
}