Skip to main content

pe_sigscan/
scan.rs

1//! Public scanning entry points + private range scanners.
2//!
3//! Two layers:
4//!
5//! - **Range scanners** ([`scan_range`], [`count_range`]) — work on any
6//!   contiguous byte range described by `(start, size)`. Shared between
7//!   the in-process scanners (which derive `(start, size)` from PE header
8//!   fields) and the slice scanners (which derive it from `&[u8]` length).
9//! - **Public entry points** — thin wrappers that derive ranges from PE
10//!   headers ([`find_in_text`], [`count_in_text`], [`find_in_exec_sections`],
11//!   [`count_in_exec_sections`]) or from a slice ([`find_in_slice`],
12//!   [`count_in_slice`]) and delegate to the range scanners.
13//!
14//! All scanners use a single-byte anchor pre-filter: the first non-wildcard
15//! byte of the pattern is sampled at every candidate offset before invoking
16//! [`matches_at`]. For typical signatures this skips ~99% of candidate
17//! offsets without paying the per-byte loop cost.
18
19use crate::fastscan::{first_byte_in_raw, first_byte_in_slice};
20use crate::pattern::WildcardPattern;
21use crate::pe::{exec_sections, text_section_bounds};
22
23/// Match `pattern` against the bytes starting at `addr`. Wildcards (`None`
24/// entries) match any byte.
25///
26/// # Safety
27///
28/// Caller must guarantee `[addr, addr + pattern.len())` is readable.
29#[inline]
30unsafe fn matches_at(addr: usize, pattern: WildcardPattern<'_>) -> bool {
31    for (i, slot) in pattern.iter().enumerate() {
32        if let Some(want) = *slot {
33            let got = *((addr + i) as *const u8);
34            if got != want {
35                return false;
36            }
37        }
38    }
39    true
40}
41
42/// Find the first occurrence of `pattern` within the named `.text` section
43/// of the PE module loaded at `module_base`.
44///
45/// Returns `None` if the pattern is empty, the module headers are
46/// malformed, the `.text` section is not present, or the pattern does not
47/// match anywhere.
48///
49/// See [`find_in_exec_sections`] for the variant that walks every
50/// executable section (use when the function may live in a companion code
51/// section like `.text$mn`).
52#[must_use]
53pub fn find_in_text(module_base: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
54    if module_base == 0 || pattern.is_empty() {
55        return None;
56    }
57    let (start, size) = text_section_bounds(module_base)?;
58    scan_range(start, size, pattern)
59}
60
61/// Count occurrences of `pattern` within the named `.text` section of the
62/// PE module loaded at `module_base`.
63///
64/// Useful before installing a hook to verify pattern uniqueness — a
65/// pattern that matches multiple functions risks hooking the wrong one and
66/// silently corrupting unrelated state. Callers should typically refuse to
67/// install when the count isn't exactly 1.
68#[must_use]
69pub fn count_in_text(module_base: usize, pattern: WildcardPattern<'_>) -> usize {
70    if module_base == 0 || pattern.is_empty() {
71        return 0;
72    }
73    match text_section_bounds(module_base) {
74        Some((start, size)) => count_range(start, size, pattern),
75        None => 0,
76    }
77}
78
79/// Find the first occurrence of `pattern` within ANY executable section of
80/// the PE module loaded at `module_base`.
81///
82/// Use this when the function may live outside the section literally named
83/// `.text`. Some compilers and linkers split code across multiple executable
84/// sections (for example `.text$mn`, `.textbss`, or optimized code arenas),
85/// and the section named `.text` may not contain the target function.
86///
87/// Same speed as [`find_in_text`] (direct in-process reads bounded to
88/// PE-declared section ranges); the only difference is the section-name
89/// filter is dropped in favour of an `IMAGE_SCN_MEM_EXECUTE`
90/// characteristic check.
91#[must_use]
92pub fn find_in_exec_sections(module_base: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
93    if module_base == 0 || pattern.is_empty() {
94        return None;
95    }
96    for (start, size) in exec_sections(module_base)? {
97        if let Some(addr) = scan_range(start, size, pattern) {
98            return Some(addr);
99        }
100    }
101    None
102}
103
104/// Count occurrences of `pattern` across ALL executable sections of the PE
105/// module loaded at `module_base`. Companion to [`find_in_exec_sections`];
106/// same hook-install uniqueness contract as [`count_in_text`].
107#[must_use]
108pub fn count_in_exec_sections(module_base: usize, pattern: WildcardPattern<'_>) -> usize {
109    if module_base == 0 || pattern.is_empty() {
110        return 0;
111    }
112    let Some(sections) = exec_sections(module_base) else {
113        return 0;
114    };
115    let mut total = 0usize;
116    for (start, size) in sections {
117        total += count_range(start, size, pattern);
118    }
119    total
120}
121
122/// Find the first occurrence of `pattern` within the slice `haystack`.
123///
124/// This variant is platform-agnostic and does not require a loaded PE
125/// module — useful for offline analysis or for testing patterns against
126/// pre-extracted byte buffers. Returns the absolute address of the match
127/// in the form `haystack.as_ptr() as usize + offset`, so the result is
128/// directly comparable to addresses returned by the in-process scanners.
129///
130/// Returns `None` if the pattern is empty, longer than `haystack`, or does
131/// not match anywhere.
132#[must_use]
133pub fn find_in_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> Option<usize> {
134    if pattern.is_empty() || haystack.len() < pattern.len() {
135        return None;
136    }
137    scan_slice(haystack, pattern).map(|off| haystack.as_ptr() as usize + off)
138}
139
140/// Count occurrences of `pattern` within the slice `haystack`. Non-
141/// overlapping: a pattern that matches at offset `i` advances the search
142/// past `i + pattern.len()` rather than `i + 1`.
143#[must_use]
144pub fn count_in_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> usize {
145    if pattern.is_empty() || haystack.len() < pattern.len() {
146        return 0;
147    }
148    count_slice(haystack, pattern)
149}
150
151/// Iterator over non-overlapping match addresses within a `&[u8]`
152/// haystack. Returned by [`iter_in_slice`].
153///
154/// Each yielded value is the absolute address `haystack.as_ptr() as
155/// usize + offset_of_match`, mirroring the return value of
156/// [`find_in_slice`].
157///
158/// Matches are non-overlapping: after a hit at offset `i` the next probe
159/// starts at `i + pattern.len()`. An empty pattern yields no matches.
160#[derive(Debug, Clone)]
161pub struct SliceMatches<'a> {
162    haystack: &'a [u8],
163    pattern: WildcardPattern<'a>,
164    /// Next byte offset within `haystack` to start scanning from.
165    cursor: usize,
166}
167
168impl<'a> Iterator for SliceMatches<'a> {
169    type Item = usize;
170
171    fn next(&mut self) -> Option<usize> {
172        let pat_len = self.pattern.len();
173        if pat_len == 0 {
174            return None;
175        }
176        let off = scan_slice_from(self.haystack, self.cursor, self.pattern)?;
177        self.cursor = off + pat_len;
178        Some(self.haystack.as_ptr() as usize + off)
179    }
180}
181
182/// Iterator over non-overlapping match addresses within one or more raw
183/// byte ranges of a loaded PE module. Returned by [`iter_in_text`] and
184/// [`iter_in_exec_sections`].
185///
186/// Each yielded value is the absolute address of a match, identical in
187/// shape to what [`find_in_text`] / [`find_in_exec_sections`] return.
188///
189/// When the iterator was constructed via [`iter_in_exec_sections`] and
190/// the current section is exhausted, the iterator transparently advances
191/// to the next executable section.
192#[derive(Debug, Clone)]
193pub struct Matches<'a> {
194    /// Remaining `(virtual_address_absolute, virtual_size)` ranges to
195    /// scan. The currently-active range is `sections[section_idx]`.
196    sections: alloc::vec::Vec<(usize, usize)>,
197    section_idx: usize,
198    /// Next byte offset within `sections[section_idx]` to scan from.
199    cursor: usize,
200    pattern: WildcardPattern<'a>,
201}
202
203impl<'a> Iterator for Matches<'a> {
204    type Item = usize;
205
206    fn next(&mut self) -> Option<usize> {
207        let pat_len = self.pattern.len();
208        if pat_len == 0 {
209            return None;
210        }
211        while self.section_idx < self.sections.len() {
212            let (start, size) = self.sections[self.section_idx];
213            if let Some(addr) = scan_range_from(start, size, self.cursor, self.pattern) {
214                let off = addr - start;
215                self.cursor = off + pat_len;
216                return Some(addr);
217            }
218            // Current section exhausted — advance to the next one.
219            self.section_idx += 1;
220            self.cursor = 0;
221        }
222        None
223    }
224}
225
226/// Iterate over every non-overlapping occurrence of `pattern` within the
227/// slice `haystack`.
228///
229/// The iterator is lazy: each `next()` call resumes the underlying
230/// SIMD/SWAR search from where the last match ended. Useful for logging
231/// every hit, applying additional per-match filters, or patching multiple
232/// call sites in one pass without rolling a manual scan loop.
233///
234/// Yields nothing if `pattern` is empty or longer than `haystack`.
235///
236/// # Examples
237///
238/// ```
239/// use pe_sigscan::{iter_in_slice, pattern};
240///
241/// let bytes = [0x48, 0x8B, 0x05, 0x00, 0x48, 0x8B, 0x05, 0xFF];
242/// let pat = pattern![0x48, 0x8B, 0x05];
243/// let hits: alloc::vec::Vec<usize> = iter_in_slice(&bytes, pat).collect();
244/// // Two non-overlapping matches at offsets 0 and 4.
245/// assert_eq!(hits.len(), 2);
246/// assert_eq!(hits[0], bytes.as_ptr() as usize);
247/// assert_eq!(hits[1], bytes.as_ptr() as usize + 4);
248/// # extern crate alloc;
249/// ```
250#[must_use]
251pub fn iter_in_slice<'a>(haystack: &'a [u8], pattern: WildcardPattern<'a>) -> SliceMatches<'a> {
252    SliceMatches {
253        haystack,
254        pattern,
255        cursor: 0,
256    }
257}
258
259/// Iterate over every non-overlapping occurrence of `pattern` within the
260/// section literally named `.text` of the PE module loaded at
261/// `module_base`.
262///
263/// Yields nothing if `module_base` is zero, `pattern` is empty, the
264/// module headers are malformed, or the `.text` section is absent.
265///
266/// See [`iter_in_exec_sections`] for the variant that walks every
267/// executable section (use when the function may live in a companion code
268/// section like `.text$mn`).
269#[must_use]
270pub fn iter_in_text<'a>(module_base: usize, pattern: WildcardPattern<'a>) -> Matches<'a> {
271    let sections = if module_base == 0 || pattern.is_empty() {
272        alloc::vec::Vec::new()
273    } else {
274        match text_section_bounds(module_base) {
275            Some(range) => alloc::vec![range],
276            None => alloc::vec::Vec::new(),
277        }
278    };
279    Matches {
280        sections,
281        section_idx: 0,
282        cursor: 0,
283        pattern,
284    }
285}
286
287/// Iterate over every non-overlapping occurrence of `pattern` across ALL
288/// executable sections of the PE module loaded at `module_base`.
289///
290/// Sections are visited in their PE section-table order; matches inside
291/// one section are exhausted before moving to the next. Yields nothing if
292/// `module_base` is zero, `pattern` is empty, or the module headers are
293/// malformed.
294#[must_use]
295pub fn iter_in_exec_sections<'a>(module_base: usize, pattern: WildcardPattern<'a>) -> Matches<'a> {
296    let sections = if module_base == 0 || pattern.is_empty() {
297        alloc::vec::Vec::new()
298    } else {
299        exec_sections(module_base).unwrap_or_default()
300    };
301    Matches {
302        sections,
303        section_idx: 0,
304        cursor: 0,
305        pattern,
306    }
307}
308
309/// Find the first occurrence of `pattern` within the section whose
310/// 8-byte name starts with `section_name`.
311///
312/// `section_name` matches `IMAGE_SECTION_HEADER.Name` as a byte
313/// prefix: `b".text"` matches `.text\0\0\0`, `.text$mn`, `.textbss`.
314/// Pass the full 8 bytes for exact-match disambiguation.
315///
316/// Returns `None` if `module_base` is zero, the pattern is empty,
317/// the section is missing, or the pattern doesn't match. Returned
318/// addresses are absolute — same shape as [`find_in_text`].
319///
320/// # Examples
321///
322/// ```no_run
323/// use pe_sigscan::{find_in_section, pattern};
324/// # let module_base = 0usize;
325///
326/// let pat = pattern![b'h', b'e', b'l', b'l', b'o', 0x00];
327/// if let Some(addr) = find_in_section(module_base, b".rdata", pat) {
328///     println!("string at {addr:#x}");
329/// }
330/// ```
331#[cfg(feature = "section-info")]
332#[must_use]
333pub fn find_in_section(
334    module_base: usize,
335    section_name: &[u8],
336    pattern: WildcardPattern<'_>,
337) -> Option<usize> {
338    if module_base == 0 || pattern.is_empty() {
339        return None;
340    }
341    let section = crate::pe::find_section(module_base, section_name)?;
342    scan_range(section.virtual_address, section.virtual_size, pattern)
343}
344
345/// Count non-overlapping occurrences of `pattern` within the named
346/// section. Companion to [`find_in_section`]; same uniqueness
347/// contract as [`count_in_text`].
348///
349/// Returns `0` if `module_base` is zero, the pattern is empty, the
350/// section is missing, or the pattern doesn't match.
351#[cfg(feature = "section-info")]
352#[must_use]
353pub fn count_in_section(
354    module_base: usize,
355    section_name: &[u8],
356    pattern: WildcardPattern<'_>,
357) -> usize {
358    if module_base == 0 || pattern.is_empty() {
359        return 0;
360    }
361    let Some(section) = crate::pe::find_section(module_base, section_name) else {
362        return 0;
363    };
364    count_range(section.virtual_address, section.virtual_size, pattern)
365}
366
367/// Iterate over every non-overlapping occurrence of `pattern` within
368/// the named section.
369///
370/// Yields nothing if `module_base` is zero, the pattern is empty, or
371/// the section is missing. Yielded addresses are absolute — same
372/// shape as [`iter_in_text`].
373#[cfg(feature = "section-info")]
374#[must_use]
375pub fn iter_in_section<'a>(
376    module_base: usize,
377    section_name: &[u8],
378    pattern: WildcardPattern<'a>,
379) -> Matches<'a> {
380    let sections = if module_base == 0 || pattern.is_empty() {
381        alloc::vec::Vec::new()
382    } else {
383        crate::pe::find_section(module_base, section_name)
384            .map(|s| alloc::vec![(s.virtual_address, s.virtual_size)])
385            .unwrap_or_default()
386    };
387    Matches {
388        sections,
389        section_idx: 0,
390        cursor: 0,
391        pattern,
392    }
393}
394
395/// Locate the first non-wildcard byte in the pattern. Returns the
396/// (offset_within_pattern, byte_value) pair, or `None` if the pattern is
397/// all wildcards (in which case the anchor pre-filter must be skipped).
398#[inline]
399fn anchor(pattern: WildcardPattern<'_>) -> Option<(usize, u8)> {
400    pattern
401        .iter()
402        .enumerate()
403        .find_map(|(i, b)| b.map(|byte| (i, byte)))
404}
405
406/// Slice-native scan for the first match starting at byte offset `from`.
407///
408/// Uses [`first_byte_in_slice`] to skip directly to the next plausible
409/// candidate offset instead of stepping byte-by-byte. On haystacks where
410/// the anchor byte is rare this is dramatically faster than
411/// [`scan_range`].
412///
413/// Returns the matching offset within `haystack` (NOT the absolute
414/// address — callers map to absolute via `haystack.as_ptr() + off`).
415///
416/// `#[inline]` is load-bearing: when `find_in_slice` calls in via
417/// `scan_slice` with the constant `from = 0`, inlining lets LLVM fold
418/// the redundant `haystack.len() < pat_len` and `from > upper` checks
419/// against the caller's pre-validated lengths, restoring the original
420/// pre-refactor codegen on the hot path. Without `#[inline]`, the
421/// inliner is right at its size heuristic and may decline to inline
422/// across both call sites (single-shot vs iterator), costing ~2-3 % on
423/// 1 MiB scans.
424#[inline]
425fn scan_slice_from(haystack: &[u8], from: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
426    let pat_len = pattern.len();
427    if haystack.len() < pat_len {
428        return None;
429    }
430    let upper = haystack.len() - pat_len;
431    if from > upper {
432        return None;
433    }
434    let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
435        // All-wildcard pattern matches at the cursor.
436        return Some(from);
437    };
438
439    let mut i = from;
440    while i <= upper {
441        // Search for the anchor byte starting from the current candidate
442        // offset (offset by `anchor_off` so the byte we find lines up
443        // correctly with the pattern). `search_from < haystack.len()` is
444        // guaranteed by `i <= upper` and `anchor_off < pat_len`.
445        let search_from = i + anchor_off;
446        let Some(rel) = first_byte_in_slice(&haystack[search_from..], anchor_byte) else {
447            return None;
448        };
449        let candidate = search_from + rel - anchor_off;
450        if candidate > upper {
451            return None;
452        }
453        // SAFETY: `candidate + pat_len <= haystack.len()` by `candidate <= upper`.
454        if unsafe { matches_at(haystack.as_ptr() as usize + candidate, pattern) } {
455            return Some(candidate);
456        }
457        i = candidate + 1;
458    }
459    None
460}
461
462/// Convenience: `scan_slice_from(haystack, 0, pattern)`. Used by the
463/// single-shot [`find_in_slice`] entry point.
464#[inline]
465fn scan_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> Option<usize> {
466    scan_slice_from(haystack, 0, pattern)
467}
468
469/// Slice-native count of non-overlapping matches.
470fn count_slice(haystack: &[u8], pattern: WildcardPattern<'_>) -> usize {
471    let pat_len = pattern.len();
472    let upper = haystack.len() - pat_len;
473    let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
474        // All-wildcard pattern: every position matches; the byte-by-byte
475        // semantics stride by `pat_len` for non-overlap, so the count is
476        // `floor(haystack.len() / pat_len)`. `pat_len >= 1` is guaranteed
477        // by the public-entry check at the top of `count_in_slice`.
478        return haystack.len() / pat_len;
479    };
480
481    let mut count = 0usize;
482    let mut i = 0usize;
483    while i <= upper {
484        // `search_from < haystack.len()` is guaranteed by `i <= upper` and
485        // `anchor_off < pat_len`.
486        let search_from = i + anchor_off;
487        let Some(rel) = first_byte_in_slice(&haystack[search_from..], anchor_byte) else {
488            break;
489        };
490        let candidate = search_from + rel - anchor_off;
491        if candidate > upper {
492            break;
493        }
494        // SAFETY: in-bounds by the same invariant.
495        if unsafe { matches_at(haystack.as_ptr() as usize + candidate, pattern) } {
496            count += 1;
497            i = candidate + pat_len;
498        } else {
499            i = candidate + 1;
500        }
501    }
502    count
503}
504
505/// Scan a contiguous raw byte range for the first match, starting at
506/// byte offset `from` within the range.
507///
508/// # Safety contract for the unsafe pointer reads
509///
510/// `start..start+size` must be a readable contiguous range of bytes. For
511/// the in-process callers this is guaranteed by the PE section bounds; for
512/// the slice variant by Rust's `&[u8]` lifetime + length invariants. The
513/// `i <= upper = size - pat_len` loop invariant ensures every read is
514/// inside the range.
515///
516/// `#[inline]` is load-bearing for the same reason as
517/// [`scan_slice_from`]: with the constant `from = 0` from `find_in_text`
518/// / `find_in_exec_sections` the redundant `from > upper` branch is
519/// statically eliminable by LLVM, but only after inlining.
520#[inline]
521fn scan_range_from(
522    start: usize,
523    size: usize,
524    from: usize,
525    pattern: WildcardPattern<'_>,
526) -> Option<usize> {
527    let pat_len = pattern.len();
528    if size < pat_len {
529        return None;
530    }
531    let upper = size - pat_len;
532    if from > upper {
533        return None;
534    }
535    let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
536        return Some(start + from);
537    };
538
539    let mut i = from;
540    while i <= upper {
541        // `search_from < size` is guaranteed by `i <= upper` and
542        // `anchor_off < pat_len`.
543        let search_from = i + anchor_off;
544        // SAFETY: `[start+search_from, start+size)` is a subset of the
545        // range the caller declared readable.
546        let Some(rel) =
547            (unsafe { first_byte_in_raw(start + search_from, size - search_from, anchor_byte) })
548        else {
549            return None;
550        };
551        let candidate = search_from + rel - anchor_off;
552        if candidate > upper {
553            return None;
554        }
555        let addr = start + candidate;
556        // SAFETY: bounds upheld by the same invariant.
557        if unsafe { matches_at(addr, pattern) } {
558            return Some(addr);
559        }
560        i = candidate + 1;
561    }
562    None
563}
564
565/// Convenience: `scan_range_from(start, size, 0, pattern)`. Used by the
566/// single-shot in-process `find_*` entry points.
567#[inline]
568fn scan_range(start: usize, size: usize, pattern: WildcardPattern<'_>) -> Option<usize> {
569    scan_range_from(start, size, 0, pattern)
570}
571
572/// Count occurrences of `pattern` within a single contiguous raw byte
573/// range.
574///
575/// Counts non-overlapping matches: when a match is found at offset `i`,
576/// the next probe starts at `i + pattern.len()`. Counting overlapping
577/// matches is not the use case this crate targets and would inflate
578/// counts for patterns with internal repetition.
579fn count_range(start: usize, size: usize, pattern: WildcardPattern<'_>) -> usize {
580    let pat_len = pattern.len();
581    if size < pat_len {
582        return 0;
583    }
584    let upper = size - pat_len;
585    let Some((anchor_off, anchor_byte)) = anchor(pattern) else {
586        // All-wildcard pattern; same reasoning as `count_slice`. `pat_len
587        // >= 1` is guaranteed by the public-entry caller.
588        return size / pat_len;
589    };
590
591    let mut count = 0usize;
592    let mut i = 0usize;
593    while i <= upper {
594        // `search_from < size` is guaranteed by `i <= upper` and
595        // `anchor_off < pat_len`.
596        let search_from = i + anchor_off;
597        // SAFETY: in-bounds for the declared range.
598        let Some(rel) =
599            (unsafe { first_byte_in_raw(start + search_from, size - search_from, anchor_byte) })
600        else {
601            break;
602        };
603        let candidate = search_from + rel - anchor_off;
604        if candidate > upper {
605            break;
606        }
607        let addr = start + candidate;
608        // SAFETY: in-bounds.
609        if unsafe { matches_at(addr, pattern) } {
610            count += 1;
611            i = candidate + pat_len;
612        } else {
613            i = candidate + 1;
614        }
615    }
616    count
617}
618
619#[cfg(test)]
620mod tests {
621    use super::*;
622    use crate::pattern;
623    use crate::pe::IMAGE_SCN_MEM_EXECUTE;
624    use alloc::vec;
625    use alloc::vec::Vec;
626
627    /// Local copy of `synthetic_pe` so this module can build PE-shaped
628    /// buffers for the in-process scanner tests without having to reach
629    /// into `pe.rs`'s test module.
630    fn synthetic_pe(sections: &[([u8; 8], u32, &[u8], u32)]) -> Vec<u8> {
631        let needed = sections
632            .iter()
633            .map(|(_, vaddr, bytes, _)| *vaddr as usize + bytes.len())
634            .max()
635            .unwrap_or(0)
636            .max(0x400);
637        let mut buf = vec![0u8; needed];
638        buf[0] = b'M';
639        buf[1] = b'Z';
640        let nt_offset: u32 = 0x80;
641        buf[0x3C..0x40].copy_from_slice(&nt_offset.to_le_bytes());
642        let nt = nt_offset as usize;
643        buf[nt..nt + 4].copy_from_slice(b"PE\0\0");
644        let num_sections: u16 = sections.len() as u16;
645        buf[nt + 4 + 2..nt + 4 + 4].copy_from_slice(&num_sections.to_le_bytes());
646        let opt_size: u16 = 0xF0;
647        buf[nt + 4 + 16..nt + 4 + 18].copy_from_slice(&opt_size.to_le_bytes());
648        let section_table = nt + 4 + 20 + opt_size as usize;
649        for (i, (name, vaddr, bytes, characteristics)) in sections.iter().enumerate() {
650            let sec = section_table + i * 40;
651            buf[sec..sec + 8].copy_from_slice(name);
652            let vsize: u32 = bytes.len() as u32;
653            buf[sec + 8..sec + 12].copy_from_slice(&vsize.to_le_bytes());
654            buf[sec + 12..sec + 16].copy_from_slice(&vaddr.to_le_bytes());
655            buf[sec + 36..sec + 40].copy_from_slice(&characteristics.to_le_bytes());
656            let v = *vaddr as usize;
657            buf[v..v + bytes.len()].copy_from_slice(bytes);
658        }
659        buf
660    }
661
662    // -- slice variants ----------------------------------------------------
663
664    #[test]
665    fn slice_find_basic() {
666        let haystack = [0x00, 0x11, 0x48, 0x8B, 0x05, 0x99, 0xAA];
667        let pat = pattern![0x48, 0x8B, 0x05];
668        let hit = find_in_slice(&haystack, pat).unwrap();
669        assert_eq!(hit, haystack.as_ptr() as usize + 2);
670    }
671
672    #[test]
673    fn slice_find_wildcard() {
674        let haystack = [0x00, 0x48, 0x77, 0x05, 0xFF];
675        let pat = pattern![0x48, _, 0x05];
676        let hit = find_in_slice(&haystack, pat).unwrap();
677        assert_eq!(hit, haystack.as_ptr() as usize + 1);
678    }
679
680    #[test]
681    fn slice_find_misses_returns_none() {
682        let haystack = [0x00, 0x11, 0x22];
683        let pat = pattern![0x48, 0x8B, 0x05];
684        assert!(find_in_slice(&haystack, pat).is_none());
685    }
686
687    #[test]
688    fn slice_find_empty_pattern_returns_none() {
689        let haystack = [0x00, 0x11];
690        let pat: &[Option<u8>] = &[];
691        assert!(find_in_slice(&haystack, pat).is_none());
692    }
693
694    #[test]
695    fn slice_find_pattern_longer_than_haystack() {
696        let haystack = [0x48];
697        let pat = pattern![0x48, 0x8B, 0x05];
698        assert!(find_in_slice(&haystack, pat).is_none());
699    }
700
701    #[test]
702    fn slice_find_all_wildcards_matches_first() {
703        // Pattern with no anchor byte exercises the `has_anchor = false`
704        // path inside `scan_range`.
705        let haystack = [0xAA, 0xBB, 0xCC];
706        let pat: &[Option<u8>] = &[None, None];
707        let hit = find_in_slice(&haystack, pat).unwrap();
708        assert_eq!(hit, haystack.as_ptr() as usize);
709    }
710
711    #[test]
712    fn slice_find_anchor_match_but_full_pattern_mismatch() {
713        // The anchor byte (0x48) appears at offset 0 where the FULL pattern
714        // does NOT match (next byte is 0xFF, not 0x8B), and again at offset
715        // 2 where it DOES match. Forces the scanner to:
716        //   * pass the anchor pre-filter at i=0
717        //   * fall into `matches_at`, which compares index 1 (0x8B vs 0xFF)
718        //     and returns false — exercising `matches_at`'s `return false`
719        //     branch
720        //   * advance i and continue searching — exercising `scan_range`'s
721        //     post-mismatch `i += 1` line
722        //   * eventually find the real match at offset 2
723        let haystack = [0x48, 0xFF, 0x48, 0x8B];
724        let pat = pattern![0x48, 0x8B];
725        let hit = find_in_slice(&haystack, pat).unwrap();
726        assert_eq!(hit, haystack.as_ptr() as usize + 2);
727    }
728
729    #[test]
730    fn slice_count_anchor_match_but_full_pattern_mismatch() {
731        // Same construction as the previous test but for the count path,
732        // exercising `count_range`'s `else { i += 1 }` branch when
733        // `matches_at` returns false after an anchor pre-filter pass.
734        let haystack = [0x48, 0xFF, 0x48, 0x8B];
735        let pat = pattern![0x48, 0x8B];
736        assert_eq!(count_in_slice(&haystack, pat), 1);
737    }
738
739    #[test]
740    fn slice_find_anchor_in_middle() {
741        // First pattern byte is a wildcard, second is the anchor. Forces
742        // `find_map` to skip past index 0 when computing the anchor.
743        let haystack = [0xCC, 0x77, 0x99, 0xAA, 0x77, 0x99];
744        let pat: &[Option<u8>] = &[None, Some(0x77), Some(0x99)];
745        let hit = find_in_slice(&haystack, pat).unwrap();
746        // First match at offset 0: pos[1]=0x77, pos[2]=0x99 → matches.
747        assert_eq!(hit, haystack.as_ptr() as usize);
748    }
749
750    #[test]
751    fn slice_count_basic() {
752        let haystack = [0x48, 0x8B, 0x00, 0x48, 0x8B, 0x00, 0x48, 0x8B];
753        let pat = pattern![0x48, 0x8B];
754        assert_eq!(count_in_slice(&haystack, pat), 3);
755    }
756
757    #[test]
758    fn slice_count_no_overlap() {
759        // Pattern repeated in haystack — non-overlapping policy means we
760        // count 2 matches (0..2 and 2..4), not 3.
761        let haystack = [0x42, 0x42, 0x42, 0x42];
762        let pat = pattern![0x42, 0x42];
763        assert_eq!(count_in_slice(&haystack, pat), 2);
764    }
765
766    #[test]
767    fn slice_count_zero_when_no_match() {
768        let haystack = [0x00, 0x11, 0x22];
769        let pat = pattern![0x48];
770        assert_eq!(count_in_slice(&haystack, pat), 0);
771    }
772
773    #[test]
774    fn slice_count_empty_pattern_returns_zero() {
775        let haystack = [0x00, 0x11];
776        let pat: &[Option<u8>] = &[];
777        assert_eq!(count_in_slice(&haystack, pat), 0);
778    }
779
780    #[test]
781    fn slice_count_pattern_longer_than_haystack() {
782        let haystack = [0x48];
783        let pat = pattern![0x48, 0x8B, 0x05];
784        assert_eq!(count_in_slice(&haystack, pat), 0);
785    }
786
787    #[test]
788    fn slice_count_all_wildcards() {
789        // No-anchor count path. Two non-overlapping length-2 windows fit
790        // in a 4-byte haystack.
791        let haystack = [0xAA, 0xBB, 0xCC, 0xDD];
792        let pat: &[Option<u8>] = &[None, None];
793        assert_eq!(count_in_slice(&haystack, pat), 2);
794    }
795
796    // -- in-process variants (synthetic PE) --------------------------------
797
798    #[test]
799    fn synthetic_pe_text_find_and_count() {
800        let text = [0x00u8, 0x11, 0x48, 0x8B, 0x05, 0xFF, 0x00, 0x48, 0x8B, 0x05];
801        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &text, IMAGE_SCN_MEM_EXECUTE)]);
802        let base = buf.as_ptr() as usize;
803        let pat = pattern![0x48, 0x8B, 0x05];
804
805        // First match is at the first hit, at offset 2 inside the section.
806        let hit = find_in_text(base, pat).unwrap();
807        assert_eq!(hit, base + 0x300 + 2);
808
809        // Two non-overlapping matches in the synthetic body.
810        assert_eq!(count_in_text(base, pat), 2);
811    }
812
813    #[test]
814    fn synthetic_pe_text_find_returns_none_when_no_match() {
815        let text = [0xAAu8, 0xBB, 0xCC];
816        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &text, IMAGE_SCN_MEM_EXECUTE)]);
817        let base = buf.as_ptr() as usize;
818        let pat = pattern![0x48, 0x8B];
819        assert!(find_in_text(base, pat).is_none());
820        assert_eq!(count_in_text(base, pat), 0);
821    }
822
823    #[test]
824    fn synthetic_pe_text_returns_none_when_no_text_section() {
825        // No `.text` section, only `.data`. text_section_bounds returns None
826        // → find/count both bail out via the early-return paths.
827        let body = [0x48u8, 0x8B];
828        let buf = synthetic_pe(&[(*b".data\0\0\0", 0x300, &body, 0)]);
829        let base = buf.as_ptr() as usize;
830        let pat = pattern![0x48, 0x8B];
831        assert!(find_in_text(base, pat).is_none());
832        assert_eq!(count_in_text(base, pat), 0);
833    }
834
835    #[test]
836    fn synthetic_pe_exec_sections_find_across_sections() {
837        // Pattern is in the SECOND executable section. find_in_exec_sections
838        // should still locate it after scanning past the first.
839        let body_a = [0xAAu8, 0xBB];
840        let body_b = [0x90u8, 0x90, 0xC3];
841        let buf = synthetic_pe(&[
842            (*b".text\0\0\0", 0x300, &body_a, IMAGE_SCN_MEM_EXECUTE),
843            (*b".text$mn", 0x310, &body_b, IMAGE_SCN_MEM_EXECUTE),
844        ]);
845        let base = buf.as_ptr() as usize;
846        let pat = pattern![0x90, 0x90, 0xC3];
847        let hit = find_in_exec_sections(base, pat).unwrap();
848        assert_eq!(hit, base + 0x310);
849        assert_eq!(count_in_exec_sections(base, pat), 1);
850    }
851
852    #[test]
853    fn synthetic_pe_exec_sections_count_sums_across_sections() {
854        // Same pattern in two executable sections. count_in_exec_sections
855        // should sum (1 + 1).
856        let body = [0x90u8, 0x90];
857        let buf = synthetic_pe(&[
858            (*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE),
859            (*b".text$mn", 0x310, &body, IMAGE_SCN_MEM_EXECUTE),
860        ]);
861        let base = buf.as_ptr() as usize;
862        let pat = pattern![0x90, 0x90];
863        assert_eq!(count_in_exec_sections(base, pat), 2);
864    }
865
866    #[test]
867    fn synthetic_pe_exec_sections_returns_none_when_no_match() {
868        let body = [0xAAu8];
869        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
870        let base = buf.as_ptr() as usize;
871        let pat = pattern![0x48];
872        assert!(find_in_exec_sections(base, pat).is_none());
873        assert_eq!(count_in_exec_sections(base, pat), 0);
874    }
875
876    // -- guard paths -------------------------------------------------------
877
878    #[test]
879    fn null_module_returns_none_or_zero() {
880        let pat = pattern![0x48];
881        assert!(find_in_text(0, pat).is_none());
882        assert_eq!(count_in_text(0, pat), 0);
883        assert!(find_in_exec_sections(0, pat).is_none());
884        assert_eq!(count_in_exec_sections(0, pat), 0);
885    }
886
887    #[test]
888    fn empty_pattern_returns_none_or_zero() {
889        let body = [0x90u8];
890        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
891        let base = buf.as_ptr() as usize;
892        let pat: &[Option<u8>] = &[];
893        assert!(find_in_text(base, pat).is_none());
894        assert_eq!(count_in_text(base, pat), 0);
895        assert!(find_in_exec_sections(base, pat).is_none());
896        assert_eq!(count_in_exec_sections(base, pat), 0);
897    }
898
899    #[test]
900    fn malformed_module_returns_none_or_zero() {
901        // Buffer is all zeros — no MZ signature → headers fail to parse →
902        // every public scan function bails out gracefully.
903        let buf = vec![0u8; 0x400];
904        let base = buf.as_ptr() as usize;
905        let pat = pattern![0x48];
906        assert!(find_in_text(base, pat).is_none());
907        assert_eq!(count_in_text(base, pat), 0);
908        assert!(find_in_exec_sections(base, pat).is_none());
909        assert_eq!(count_in_exec_sections(base, pat), 0);
910    }
911
912    // -- raw-pointer all-wildcard fast path -------------------------------
913
914    /// All-wildcard pattern via the in-process API exercises the
915    /// `scan_range`/`count_range` "no anchor" early-return branches.
916    #[test]
917    fn synthetic_pe_text_all_wildcard_pattern() {
918        let text = [0xAAu8; 16];
919        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &text, IMAGE_SCN_MEM_EXECUTE)]);
920        let base = buf.as_ptr() as usize;
921        let pat: &[Option<u8>] = &[None, None, None, None];
922
923        // Find returns the section start (offset 0).
924        let hit = find_in_text(base, pat).unwrap();
925        let (text_start, _) = crate::pe::text_section_bounds(base).unwrap();
926        assert_eq!(hit, text_start);
927
928        // Count: floor(section_size / pat_len). Section is padded to its
929        // declared VirtualSize, not just the body length, so we just check
930        // the result is positive and divides cleanly.
931        let count = count_in_text(base, pat);
932        assert!(count >= text.len() / 4);
933    }
934
935    // -- candidate-past-upper break path ----------------------------------
936
937    /// Anchor byte appears in the trailing window where the full pattern
938    /// no longer fits — `scan_slice` / `count_slice` must take the
939    /// `candidate > upper` early-return / break path.
940    #[test]
941    fn slice_anchor_at_tail_no_room_for_pattern() {
942        // Anchor is 0x48; pattern length is 4. Plant 0x48 in the last byte
943        // so candidate would be haystack.len()-1 > upper = haystack.len()-4.
944        let mut buf = vec![0u8; 16];
945        buf[15] = 0x48;
946        let pat = pattern![0x48, 0x8B, 0x05, _];
947
948        assert!(crate::find_in_slice(&buf, pat).is_none());
949        assert_eq!(crate::count_in_slice(&buf, pat), 0);
950    }
951
952    /// `scan_slice` natural fall-through path: the anchor pre-filter
953    /// passes at `candidate == upper` but `matches_at` rejects the
954    /// candidate, leaving `i = candidate + 1 > upper` so the while loop
955    /// exits without ever returning, falling through to the trailing
956    /// `None`.
957    #[test]
958    fn slice_find_loop_exhausts_when_last_candidate_fails() {
959        // pat_len = 2, haystack.len() = 4, upper = 2. 0x48 only at index 2.
960        // matches_at(2) fails because byte 3 is 0xFF, not 0x8B.
961        let haystack = [0x00, 0x00, 0x48, 0xFF];
962        let pat = pattern![0x48, 0x8B];
963        assert!(find_in_slice(&haystack, pat).is_none());
964    }
965
966    // -- raw-pointer "anchor matches, full pattern doesn't" ---------------
967
968    /// Forces the in-process `scan_range` / `count_range` scanners through
969    /// the `matches_at == false` branch (raw-pointer `i = candidate + 1`),
970    /// and through the `candidate > upper` break path when the anchor
971    /// hits in the trailing window where the pattern no longer fits.
972    #[test]
973    fn synthetic_pe_text_anchor_match_but_full_pattern_mismatch() {
974        // Anchor 0x48 appears at offset 0 (full pattern fails) and at
975        // offset 4 which is past the upper bound for pat_len = 4 in a
976        // 5-byte body — exercising both `i = candidate + 1` (raw path)
977        // and `candidate > upper` (raw path) in one test.
978        let body = [0x48, 0x00, 0x00, 0x00, 0x48];
979        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
980        let base = buf.as_ptr() as usize;
981        let pat = pattern![0x48, 0xAA, 0xBB, 0xCC];
982
983        assert!(find_in_text(base, pat).is_none());
984        assert_eq!(count_in_text(base, pat), 0);
985    }
986
987    /// Companion to the above: anchor matches at exactly `upper` but the
988    /// full pattern doesn't, so `scan_range`'s while loop exits naturally
989    /// via the trailing `None`, and `count_range`'s while loop exits with
990    /// `count == 0`.
991    #[test]
992    fn synthetic_pe_text_anchor_at_upper_then_loop_exhausts() {
993        // pat_len = 4, body.len() = 4, upper = 0. Anchor at 0, full
994        // pattern fails (0x48 OK, 0x00 != 0xAA), i = 1, loop exits.
995        let body = [0x48, 0x00, 0x00, 0x00];
996        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
997        let base = buf.as_ptr() as usize;
998        let pat = pattern![0x48, 0xAA, 0xBB, 0xCC];
999
1000        assert!(find_in_text(base, pat).is_none());
1001        assert_eq!(count_in_text(base, pat), 0);
1002    }
1003
1004    // -- iterator (slice) -------------------------------------------------
1005
1006    #[test]
1007    fn iter_slice_yields_all_non_overlapping_matches() {
1008        let bytes = [
1009            0x48, 0x8B, 0x05, 0x00, 0x48, 0x8B, 0x05, 0xFF, 0x48, 0x8B, 0x05,
1010        ];
1011        let pat = pattern![0x48, 0x8B, 0x05];
1012        let hits: Vec<usize> = iter_in_slice(&bytes, pat).collect();
1013        assert_eq!(hits.len(), 3);
1014        assert_eq!(hits[0], bytes.as_ptr() as usize);
1015        assert_eq!(hits[1], bytes.as_ptr() as usize + 4);
1016        assert_eq!(hits[2], bytes.as_ptr() as usize + 8);
1017    }
1018
1019    #[test]
1020    fn iter_slice_count_matches_count_in_slice() {
1021        // The iterator and `count_in_slice` use the same non-overlap rule,
1022        // so `iter_in_slice(..).count()` must equal `count_in_slice(..)`.
1023        let bytes = [0x42, 0x42, 0x42, 0x42, 0x42];
1024        let pat = pattern![0x42, 0x42];
1025        assert_eq!(
1026            iter_in_slice(&bytes, pat).count(),
1027            count_in_slice(&bytes, pat)
1028        );
1029    }
1030
1031    #[test]
1032    fn iter_slice_empty_pattern_yields_nothing() {
1033        let bytes = [0x00, 0x11, 0x22];
1034        let pat: &[Option<u8>] = &[];
1035        assert_eq!(iter_in_slice(&bytes, pat).count(), 0);
1036    }
1037
1038    #[test]
1039    fn iter_slice_no_match_yields_nothing() {
1040        let bytes = [0xAA, 0xBB, 0xCC];
1041        let pat = pattern![0x48, 0x8B];
1042        assert_eq!(iter_in_slice(&bytes, pat).count(), 0);
1043    }
1044
1045    #[test]
1046    fn iter_slice_pattern_longer_than_haystack_yields_nothing() {
1047        let bytes = [0x48];
1048        let pat = pattern![0x48, 0x8B, 0x05];
1049        assert_eq!(iter_in_slice(&bytes, pat).count(), 0);
1050    }
1051
1052    #[test]
1053    fn iter_slice_with_wildcards() {
1054        // Pattern `48 ?? 05` should match at offsets 0 (48 8B 05) and
1055        // 3 (48 99 05), but NOT at offset 6 (48 AA FF — 3rd byte not 05).
1056        let bytes = [0x48, 0x8B, 0x05, 0x48, 0x99, 0x05, 0x48, 0xAA, 0xFF];
1057        let pat = pattern![0x48, _, 0x05];
1058        let hits: Vec<usize> = iter_in_slice(&bytes, pat).collect();
1059        assert_eq!(hits.len(), 2);
1060        assert_eq!(hits[0], bytes.as_ptr() as usize);
1061        assert_eq!(hits[1], bytes.as_ptr() as usize + 3);
1062    }
1063
1064    #[test]
1065    fn iter_slice_all_wildcard_pattern_strides_by_pat_len() {
1066        // All-wildcard pattern of length 2 in a 5-byte haystack: yields
1067        // matches at 0 and 2 (non-overlap). Offset 4 has only one byte
1068        // remaining so the pattern doesn't fit.
1069        let bytes = [0xAA, 0xBB, 0xCC, 0xDD, 0xEE];
1070        let pat: &[Option<u8>] = &[None, None];
1071        let hits: Vec<usize> = iter_in_slice(&bytes, pat).collect();
1072        assert_eq!(hits.len(), 2);
1073        assert_eq!(hits[0], bytes.as_ptr() as usize);
1074        assert_eq!(hits[1], bytes.as_ptr() as usize + 2);
1075    }
1076
1077    #[test]
1078    fn iter_slice_clone_is_independent() {
1079        // The Clone impl on the iterator must not share state with its
1080        // origin — both clones should yield the same sequence.
1081        let bytes = [0x48, 0x8B, 0x00, 0x48, 0x8B];
1082        let pat = pattern![0x48, 0x8B];
1083        let it = iter_in_slice(&bytes, pat);
1084        let from_clone: Vec<usize> = it.clone().collect();
1085        let from_original: Vec<usize> = it.collect();
1086        assert_eq!(from_clone, from_original);
1087        assert_eq!(from_clone.len(), 2);
1088    }
1089
1090    // -- iterator (in-process .text) --------------------------------------
1091
1092    #[test]
1093    fn iter_in_text_yields_all_matches() {
1094        let body = [0x48u8, 0x8B, 0x05, 0x00, 0x48, 0x8B, 0x05];
1095        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1096        let base = buf.as_ptr() as usize;
1097        let pat = pattern![0x48, 0x8B, 0x05];
1098        let hits: Vec<usize> = iter_in_text(base, pat).collect();
1099        assert_eq!(hits.len(), 2);
1100        assert_eq!(hits[0], base + 0x300);
1101        assert_eq!(hits[1], base + 0x300 + 4);
1102    }
1103
1104    #[test]
1105    fn iter_in_text_first_matches_find_in_text() {
1106        // The first iterator yield must equal the single-shot `find_in_text`
1107        // result — both use the same scan_range_from primitive.
1108        let body = [0x90u8, 0x90, 0x48, 0x8B, 0x05, 0xCC];
1109        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1110        let base = buf.as_ptr() as usize;
1111        let pat = pattern![0x48, 0x8B, 0x05];
1112        let from_iter = iter_in_text(base, pat).next();
1113        let from_find = find_in_text(base, pat);
1114        assert_eq!(from_iter, from_find);
1115        assert_eq!(from_iter, Some(base + 0x300 + 2));
1116    }
1117
1118    #[test]
1119    fn iter_in_text_count_matches_count_in_text() {
1120        // Iterator length must equal count_in_text result.
1121        let body = [0x48u8, 0x8B, 0x00, 0x48, 0x8B, 0x00, 0x48, 0x8B];
1122        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1123        let base = buf.as_ptr() as usize;
1124        let pat = pattern![0x48, 0x8B];
1125        assert_eq!(iter_in_text(base, pat).count(), count_in_text(base, pat));
1126    }
1127
1128    #[test]
1129    fn iter_in_text_null_module_yields_nothing() {
1130        let pat = pattern![0x48];
1131        assert_eq!(iter_in_text(0, pat).count(), 0);
1132    }
1133
1134    #[test]
1135    fn iter_in_text_empty_pattern_yields_nothing() {
1136        let body = [0x90u8];
1137        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1138        let base = buf.as_ptr() as usize;
1139        let pat: &[Option<u8>] = &[];
1140        assert_eq!(iter_in_text(base, pat).count(), 0);
1141    }
1142
1143    #[test]
1144    fn iter_in_text_missing_text_section_yields_nothing() {
1145        // Module has only `.data`, no `.text` — iterator returns empty.
1146        let body = [0x48u8, 0x8B];
1147        let buf = synthetic_pe(&[(*b".data\0\0\0", 0x300, &body, 0)]);
1148        let base = buf.as_ptr() as usize;
1149        let pat = pattern![0x48, 0x8B];
1150        assert_eq!(iter_in_text(base, pat).count(), 0);
1151    }
1152
1153    #[test]
1154    fn iter_in_text_malformed_module_yields_nothing() {
1155        // No MZ → text_section_bounds returns None → empty sections list.
1156        let buf = vec![0u8; 0x400];
1157        let base = buf.as_ptr() as usize;
1158        let pat = pattern![0x48];
1159        assert_eq!(iter_in_text(base, pat).count(), 0);
1160    }
1161
1162    // -- iterator (in-process all exec sections) --------------------------
1163
1164    #[test]
1165    fn iter_in_exec_sections_yields_across_multiple_sections() {
1166        let body_a = [0x90u8, 0x90, 0xC3];
1167        let body_b = [0x48u8, 0x8B, 0x05, 0xCC, 0x48, 0x8B, 0x05];
1168        let buf = synthetic_pe(&[
1169            (*b".text\0\0\0", 0x300, &body_a, IMAGE_SCN_MEM_EXECUTE),
1170            (*b".text$mn", 0x310, &body_b, IMAGE_SCN_MEM_EXECUTE),
1171        ]);
1172        let base = buf.as_ptr() as usize;
1173        let pat = pattern![0x48, 0x8B, 0x05];
1174        let hits: Vec<usize> = iter_in_exec_sections(base, pat).collect();
1175        // Two matches, both in the second exec section.
1176        assert_eq!(hits.len(), 2);
1177        assert_eq!(hits[0], base + 0x310);
1178        assert_eq!(hits[1], base + 0x310 + 4);
1179    }
1180
1181    #[test]
1182    fn iter_in_exec_sections_advances_to_next_section_after_exhaustion() {
1183        // First section has zero matches, second has one — iterator must
1184        // skip past the first cleanly.
1185        let body_a = [0xAAu8, 0xBB, 0xCC];
1186        let body_b = [0x90u8, 0x90, 0xC3];
1187        let buf = synthetic_pe(&[
1188            (*b".text\0\0\0", 0x300, &body_a, IMAGE_SCN_MEM_EXECUTE),
1189            (*b".text$mn", 0x310, &body_b, IMAGE_SCN_MEM_EXECUTE),
1190        ]);
1191        let base = buf.as_ptr() as usize;
1192        let pat = pattern![0x90, 0x90, 0xC3];
1193        let hits: Vec<usize> = iter_in_exec_sections(base, pat).collect();
1194        assert_eq!(hits, vec![base + 0x310]);
1195    }
1196
1197    #[test]
1198    fn iter_in_exec_sections_count_sums_across_sections() {
1199        let body = [0x90u8, 0x90];
1200        let buf = synthetic_pe(&[
1201            (*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE),
1202            (*b".text$mn", 0x310, &body, IMAGE_SCN_MEM_EXECUTE),
1203        ]);
1204        let base = buf.as_ptr() as usize;
1205        let pat = pattern![0x90, 0x90];
1206        // One match per section, two sections → iterator length = 2.
1207        assert_eq!(
1208            iter_in_exec_sections(base, pat).count(),
1209            count_in_exec_sections(base, pat)
1210        );
1211        assert_eq!(iter_in_exec_sections(base, pat).count(), 2);
1212    }
1213
1214    #[test]
1215    fn iter_in_exec_sections_null_module_yields_nothing() {
1216        let pat = pattern![0x48];
1217        assert_eq!(iter_in_exec_sections(0, pat).count(), 0);
1218    }
1219
1220    #[test]
1221    fn iter_in_exec_sections_empty_pattern_yields_nothing() {
1222        let body = [0x90u8];
1223        let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1224        let base = buf.as_ptr() as usize;
1225        let pat: &[Option<u8>] = &[];
1226        assert_eq!(iter_in_exec_sections(base, pat).count(), 0);
1227    }
1228
1229    #[test]
1230    fn iter_in_exec_sections_malformed_module_yields_nothing() {
1231        let buf = vec![0u8; 0x400];
1232        let base = buf.as_ptr() as usize;
1233        let pat = pattern![0x48];
1234        assert_eq!(iter_in_exec_sections(base, pat).count(), 0);
1235    }
1236
1237    #[cfg(feature = "section-info")]
1238    mod section_info_tests {
1239        use super::synthetic_pe;
1240        use crate::pattern;
1241        use crate::pe::IMAGE_SCN_MEM_EXECUTE;
1242        use crate::scan::{count_in_section, find_in_section, iter_in_section};
1243        use alloc::vec;
1244        use alloc::vec::Vec;
1245
1246        /// Two-section PE used by every bounds-sanity test:
1247        ///
1248        /// - `.text  @ 0x300`: `90 AA BB CC DD C3` (only `[AA BB CC DD]` and
1249        ///   `[AA BB]` substrings).
1250        /// - `.rdata @ 0x400`: `00 11 22 33 44 11 22 FF` (two `[11 22]`
1251        ///   matches at offsets +0x401 and +0x405).
1252        ///
1253        /// Patterns are picked so byte values in one section never collide
1254        /// with the other — bleed across section boundaries shows up
1255        /// directly as a wrong count.
1256        fn multi_section_pe() -> Vec<u8> {
1257            let text_body = [0x90u8, 0xAA, 0xBB, 0xCC, 0xDD, 0xC3];
1258            let rdata_body = [0x00u8, 0x11, 0x22, 0x33, 0x44, 0x11, 0x22, 0xFF];
1259            synthetic_pe(&[
1260                (*b".text\0\0\0", 0x300, &text_body, IMAGE_SCN_MEM_EXECUTE),
1261                (*b".rdata\0\0", 0x400, &rdata_body, 0),
1262            ])
1263        }
1264
1265        // -- find_in_section -----------------------------------------------
1266
1267        #[test]
1268        fn returns_match_in_named_section() {
1269            let buf = multi_section_pe();
1270            let base = buf.as_ptr() as usize;
1271            let pat = pattern![0x11, 0x22, 0x33];
1272            assert_eq!(find_in_section(base, b".rdata", pat), Some(base + 0x401));
1273        }
1274
1275        #[test]
1276        fn does_not_cross_section_bounds() {
1277            // Pattern lives only in .text — querying .rdata must miss,
1278            // querying .text must hit. Catches the regression where a
1279            // section-targeted scanner accidentally walks the whole image.
1280            let buf = multi_section_pe();
1281            let base = buf.as_ptr() as usize;
1282            let pat = pattern![0xAA, 0xBB, 0xCC, 0xDD];
1283            assert!(find_in_section(base, b".rdata", pat).is_none());
1284            assert_eq!(find_in_section(base, b".text", pat), Some(base + 0x301));
1285        }
1286
1287        #[test]
1288        fn matches_section_name_by_prefix() {
1289            // ".rdata$z" suffix-tagged section caught by ".rdata" query.
1290            let body = [0xDEu8, 0xAD, 0xBE, 0xEF];
1291            let buf = synthetic_pe(&[(*b".rdata$z", 0x300, &body, 0)]);
1292            let base = buf.as_ptr() as usize;
1293            let pat = pattern![0xDE, 0xAD, 0xBE, 0xEF];
1294            assert_eq!(find_in_section(base, b".rdata", pat), Some(base + 0x300));
1295        }
1296
1297        #[test]
1298        fn full_eight_byte_name_disambiguates() {
1299            // Two sections both starting with ".text"; the 8-byte query
1300            // must hit ".text\0\0\0" exactly and skip ".text$mn".
1301            let mn_body = [0x11u8, 0x22, 0x33];
1302            let text_body = [0xAAu8, 0xBB, 0xCC];
1303            let buf = synthetic_pe(&[
1304                (*b".text$mn", 0x300, &mn_body, IMAGE_SCN_MEM_EXECUTE),
1305                (*b".text\0\0\0", 0x400, &text_body, IMAGE_SCN_MEM_EXECUTE),
1306            ]);
1307            let base = buf.as_ptr() as usize;
1308            // Querying with the full 8-byte name lands on the second section.
1309            let pat = pattern![0xAA, 0xBB, 0xCC];
1310            assert_eq!(
1311                find_in_section(base, b".text\0\0\0", pat),
1312                Some(base + 0x400),
1313            );
1314            // Bytes from the .text\0\0\0 section are absent from .text$mn.
1315            assert!(find_in_section(base, b".text$mn", pat).is_none());
1316        }
1317
1318        #[test]
1319        fn returns_none_when_section_missing() {
1320            let body = [0x90u8];
1321            let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1322            let base = buf.as_ptr() as usize;
1323            let pat = pattern![0x90];
1324            assert!(find_in_section(base, b".rdata", pat).is_none());
1325        }
1326
1327        #[test]
1328        fn returns_none_when_pattern_absent() {
1329            let buf = multi_section_pe();
1330            let base = buf.as_ptr() as usize;
1331            let pat = pattern![0xFF, 0xFF, 0xFF, 0xFF];
1332            assert!(find_in_section(base, b".rdata", pat).is_none());
1333        }
1334
1335        #[test]
1336        fn null_module_returns_none() {
1337            let pat = pattern![0x90];
1338            assert!(find_in_section(0, b".rdata", pat).is_none());
1339        }
1340
1341        #[test]
1342        fn empty_pattern_returns_none() {
1343            let buf = multi_section_pe();
1344            let base = buf.as_ptr() as usize;
1345            let pat: &[Option<u8>] = &[];
1346            assert!(find_in_section(base, b".rdata", pat).is_none());
1347        }
1348
1349        #[test]
1350        fn malformed_module_returns_none() {
1351            let buf = vec![0u8; 0x400];
1352            let base = buf.as_ptr() as usize;
1353            let pat = pattern![0x90];
1354            assert!(find_in_section(base, b".rdata", pat).is_none());
1355        }
1356
1357        // -- count_in_section ----------------------------------------------
1358
1359        #[test]
1360        fn count_finds_all_matches_in_section() {
1361            let buf = multi_section_pe();
1362            let base = buf.as_ptr() as usize;
1363            // [11 22] appears twice in .rdata (offsets +0x401, +0x405).
1364            let pat = pattern![0x11, 0x22];
1365            assert_eq!(count_in_section(base, b".rdata", pat), 2);
1366        }
1367
1368        #[test]
1369        fn count_does_not_include_other_sections() {
1370            let buf = multi_section_pe();
1371            let base = buf.as_ptr() as usize;
1372            // [AA BB] is in .text only.
1373            let pat = pattern![0xAA, 0xBB];
1374            assert_eq!(count_in_section(base, b".rdata", pat), 0);
1375            assert_eq!(count_in_section(base, b".text", pat), 1);
1376        }
1377
1378        #[test]
1379        fn count_returns_zero_when_section_missing() {
1380            let body = [0x90u8];
1381            let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1382            let base = buf.as_ptr() as usize;
1383            let pat = pattern![0x90];
1384            assert_eq!(count_in_section(base, b".rdata", pat), 0);
1385        }
1386
1387        #[test]
1388        fn count_null_module_returns_zero() {
1389            let pat = pattern![0x90];
1390            assert_eq!(count_in_section(0, b".rdata", pat), 0);
1391        }
1392
1393        #[test]
1394        fn count_empty_pattern_returns_zero() {
1395            let buf = multi_section_pe();
1396            let base = buf.as_ptr() as usize;
1397            let pat: &[Option<u8>] = &[];
1398            assert_eq!(count_in_section(base, b".rdata", pat), 0);
1399        }
1400
1401        #[test]
1402        fn count_malformed_module_returns_zero() {
1403            let buf = vec![0u8; 0x400];
1404            let base = buf.as_ptr() as usize;
1405            let pat = pattern![0x90];
1406            assert_eq!(count_in_section(base, b".rdata", pat), 0);
1407        }
1408
1409        // -- iter_in_section -----------------------------------------------
1410
1411        #[test]
1412        fn iter_yields_all_matches_in_order() {
1413            let buf = multi_section_pe();
1414            let base = buf.as_ptr() as usize;
1415            let pat = pattern![0x11, 0x22];
1416            let hits: Vec<usize> = iter_in_section(base, b".rdata", pat).collect();
1417            assert_eq!(hits, vec![base + 0x401, base + 0x405]);
1418        }
1419
1420        #[test]
1421        fn iter_first_equals_find_in_section() {
1422            let buf = multi_section_pe();
1423            let base = buf.as_ptr() as usize;
1424            let pat = pattern![0x11, 0x22];
1425            assert_eq!(
1426                iter_in_section(base, b".rdata", pat).next(),
1427                find_in_section(base, b".rdata", pat),
1428            );
1429        }
1430
1431        #[test]
1432        fn iter_count_equals_count_in_section() {
1433            let buf = multi_section_pe();
1434            let base = buf.as_ptr() as usize;
1435            let pat = pattern![0x11, 0x22];
1436            assert_eq!(
1437                iter_in_section(base, b".rdata", pat).count(),
1438                count_in_section(base, b".rdata", pat),
1439            );
1440        }
1441
1442        #[test]
1443        fn iter_does_not_cross_section_bounds() {
1444            let buf = multi_section_pe();
1445            let base = buf.as_ptr() as usize;
1446            let pat = pattern![0xAA, 0xBB];
1447            assert_eq!(iter_in_section(base, b".rdata", pat).count(), 0);
1448        }
1449
1450        #[test]
1451        fn iter_section_missing_yields_nothing() {
1452            let body = [0x90u8];
1453            let buf = synthetic_pe(&[(*b".text\0\0\0", 0x300, &body, IMAGE_SCN_MEM_EXECUTE)]);
1454            let base = buf.as_ptr() as usize;
1455            let pat = pattern![0x90];
1456            assert_eq!(iter_in_section(base, b".rdata", pat).count(), 0);
1457        }
1458
1459        #[test]
1460        fn iter_null_module_yields_nothing() {
1461            let pat = pattern![0x90];
1462            assert_eq!(iter_in_section(0, b".rdata", pat).count(), 0);
1463        }
1464
1465        #[test]
1466        fn iter_empty_pattern_yields_nothing() {
1467            let buf = multi_section_pe();
1468            let base = buf.as_ptr() as usize;
1469            let pat: &[Option<u8>] = &[];
1470            assert_eq!(iter_in_section(base, b".rdata", pat).count(), 0);
1471        }
1472
1473        #[test]
1474        fn iter_malformed_module_yields_nothing() {
1475            let buf = vec![0u8; 0x400];
1476            let base = buf.as_ptr() as usize;
1477            let pat = pattern![0x90];
1478            assert_eq!(iter_in_section(base, b".rdata", pat).count(), 0);
1479        }
1480    }
1481}