./
Coverage:
low: ≥ 0%
medium: ≥ 75.0%
high: ≥ 90.0%
Lines:
43 / 52
82.7%
Branches:
37 / 50
74.0%
Functions:
5 / 5
100.0%

src/scan_matching.cpp
Line Branch Exec Source
1 /**
2 * @file scan_matching.cpp
3 * @brief Public single-pattern matching: scan(), unchecked::find_pattern(), active_simd_level(), and
4 * is_likely_function_prologue().
5 * @details scan.hpp owns each contract.
6 */
7
8 #include "DetourModKit/scan.hpp"
9
10 #include "internal/memory_guarded.hpp"
11 #include "internal/scan_engine.hpp"
12 #include "internal/scan_exclusions.hpp"
13 #include "internal/scan_pages.hpp"
14 #include "internal/scan_shared.hpp"
15
16 #include <cstddef>
17 #include <cstdint>
18 #include <new>
19 #include <span>
20
21 namespace DetourModKit
22 {
23 namespace scan
24 {
25 4 SimdLevel active_simd_level() noexcept
26 {
27 4 return detail::active_simd_level();
28 }
29
30 14 Result<Address> scan(const Pattern &pattern, Region scope, std::size_t occurrence, Pages pages) noexcept
31 {
32 14 return scan(pattern, scope, std::span<const Region>{}, occurrence, pages);
33 }
34
35 17 Result<Address> scan(
36 const Pattern &pattern,
37 Region scope,
38 std::span<const Region> exclusions,
39 std::size_t occurrence,
40 Pages pages
41 ) noexcept
42 {
43
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 6 taken 17 times.
17 if (occurrence == 0)
44 {
45 ✗ return std::unexpected(Error{ErrorCode::NoMatch, "scan::scan"});
46 }
47
4/4
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 11 taken 15 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 11 taken 1 time.
17 if (pages != Pages::Readable && pages != Pages::Executable)
48 {
49 1 return std::unexpected(Error{ErrorCode::InvalidArg, "scan::scan"});
50 }
51 16 const detail::ModuleSpan range = detail::module_span(scope);
52
2/2
✓ Branch 13 → 14 taken 1 time.
✓ Branch 13 → 17 taken 15 times.
16 if (!range.valid())
53 {
54 1 return std::unexpected(Error{ErrorCode::InvalidRange, "scan::scan"});
55 }
56
2/2
✓ Branch 18 → 19 taken 2 times.
✓ Branch 18 → 22 taken 13 times.
15 if (!detail::readable_scan_is_authoritative(range, pages, exclusions))
57 {
58 2 return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::scan"});
59 }
60 try
61 {
62 13 detail::ScanExclusions excluded;
63 // Only spans the sweep will actually read can affect its result, so dropping the rest keeps a caller
64 // that declares many copies from exhausting the set over spans that were never in play.
65 13 excluded.restrict_to(range.base, range.end);
66 13 detail::add_pattern_storage(excluded, pattern);
67 13 detail::add_regions(excluded, exclusions);
68
1/2
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 13 times.
13 if (excluded.overflowed())
69 {
70 // More declared spans than the set can hold: some query storage would go unexcluded, so no result
71 // from this scan is trustworthy. Fail closed instead of silently narrowing the exclusion.
72 ✗ return std::unexpected(Error{ErrorCode::NotAuthoritative, "scan::scan"});
73 }
74
75 13 const detail::HaystackHistogram histogram = detail::sample_haystack(scope);
76
1/2
✓ Branch 31 → 32 taken 13 times.
✗ Branch 31 → 52 not taken.
13 const detail::EnginePattern compiled = detail::to_engine_pattern(pattern, histogram);
77 13 const detail::MatchResult result = detail::scan_module_pages(
78 compiled,
79 range,
80 pages,
81 13 detail::ScanQuery{
82 .occurrence = occurrence,
83 .count_beyond = false,
84 .exclusions = &excluded,
85 }
86 );
87 // A truncated sweep never visited part of the scope, so an earlier occurrence may hide there: the match
88 // it did find is not provably the Nth one. That makes truncation fatal to the result whether or not a
89 // match was found. The two causes are distinct caller problems and stay distinct codes: a concurrent
90 // unmap of the scanned range versus a pattern whose bounded jumps are too broad to search exhaustively.
91
2/2
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 37 taken 11 times.
13 if (result.budget_exhausted)
92 {
93 2 return std::unexpected(Error{ErrorCode::BudgetExceeded, "scan::scan"});
94 }
95
1/2
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 41 taken 11 times.
11 if (result.incomplete)
96 {
97 ✗ return std::unexpected(Error{ErrorCode::IncompleteScan, "scan::scan"});
98 }
99
2/2
✓ Branch 41 → 42 taken 4 times.
✓ Branch 41 → 45 taken 7 times.
11 if (result.match == nullptr)
100 {
101 4 return std::unexpected(Error{ErrorCode::NoMatch, "scan::scan"});
102 }
103 7 return Address{reinterpret_cast<std::uintptr_t>(result.match)};
104 13 }
105
0/2
✗ Branch 53 → 54 not taken.
✗ Branch 53 → 55 not taken.
✗ catch (const std::bad_alloc &)
106 {
107 ✗ return std::unexpected(Error{ErrorCode::OutOfMemory, "scan::scan"});
108 ✗ }
109 }
110
111 17 bool is_likely_function_prologue(Address addr) noexcept
112 {
113
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 16 times.
17 if (!addr)
114 {
115 1 return false;
116 }
117
118 16 const auto b0 = detail::guarded_read<std::uint8_t>(addr.raw());
119
2/2
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 15 times.
16 if (!b0)
120 {
121 1 return false;
122 }
123
124 // is_likely_function_prologue in scan.hpp owns the poison list.
125
8/8
✓ Branch 11 → 12 taken 14 times.
✓ Branch 11 → 19 taken 1 time.
✓ Branch 13 → 14 taken 13 times.
✓ Branch 13 → 19 taken 1 time.
✓ Branch 15 → 16 taken 12 times.
✓ Branch 15 → 19 taken 1 time.
✓ Branch 17 → 18 taken 11 times.
✓ Branch 17 → 19 taken 1 time.
15 return *b0 != 0x00 && *b0 != 0xCC && *b0 != 0xC2 && *b0 != 0xC3;
126 }
127
128 namespace unchecked
129 {
130 266 const std::byte *find_pattern(Region region, const Pattern &pattern, std::size_t occurrence) noexcept
131 {
132
6/8
✓ Branch 3 → 4 taken 266 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 266 times.
✗ Branch 4 → 6 not taken.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 7 taken 265 times.
✓ Branch 8 → 9 taken 1 time.
✓ Branch 8 → 10 taken 265 times.
266 if (region.base.raw() == 0 || region.size == 0 || occurrence == 0)
133 {
134 1 return nullptr;
135 }
136 try
137 {
138 // No page filter and no haystack anchor override: the caller owns readability.
139
1/2
✓ Branch 11 → 12 taken 265 times.
✗ Branch 11 → 13 not taken.
265 const std::size_t anchor = pattern.has_anchor() ? pattern.anchor_index() : pattern.size();
140
1/2
✓ Branch 14 → 15 taken 265 times.
✗ Branch 14 → 23 not taken.
265 const detail::EnginePattern compiled = detail::engine_pattern_from(pattern, anchor);
141
1/2
✓ Branch 16 → 17 taken 265 times.
✗ Branch 16 → 21 not taken.
265 return detail::find_pattern(region.base.ptr<const std::byte>(), region.size, compiled, occurrence);
142 265 }
143
0/2
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
✗ catch (const std::bad_alloc &)
144 {
145 ✗ return nullptr;
146 ✗ }
147 }
148 } // namespace unchecked
149 } // namespace scan
150 } // namespace DetourModKit
151