GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 96.4% 27 / 0 / 28
Functions: 100.0% 4 / 0 / 4
Branches: 78.8% 26 / 0 / 33

include/DetourModKit/profile.hpp
Line Branch Exec Source
1 #ifndef DETOURMODKIT_PROFILE_HPP
2 #define DETOURMODKIT_PROFILE_HPP
3
4 /**
5 * @file profile.hpp
6 * @brief Per-game scan-tuning control plane.
7 * @details A @ref DetourModKit::ScanProfile bundles a few setup-only defaults a consumer can vary per game: whether the
8 * broad string-xref sweep is on by default, the cascade candidate-ordering preference, and a backend
9 * deny-list. It is a value type with no hot-path role and no hidden global state: a profile only supplies
10 * defaults, so explicitly declared per-call options stay visible at the call site. The deny-list fails closed
11 * -- a denied backend reports a typed failure, never a silent substitution of a different (possibly wrong)
12 * target. This header sits above both
13 * @ref scanner.hpp and @ref anchors.hpp because the profile spans the Scanner per-call options and the
14 * Anchors backend kinds; neither of those headers depends on this one.
15 */
16
17 #include "DetourModKit/anchors.hpp"
18 #include "DetourModKit/scanner.hpp"
19
20 #include <array>
21 #include <cstddef>
22 #include <cstdint>
23 #include <span>
24 #include <string_view>
25
26 namespace DetourModKit
27 {
28 /**
29 * @enum CandidateOrder
30 * @brief Cascade candidate-ordering preference a @ref ScanProfile supplies as a default.
31 */
32 enum class CandidateOrder : std::uint8_t
33 {
34 /// Try candidates exactly in declared array order (the existing behaviour).
35 AsDeclared,
36 /// Try strictly-unique (require_unique) candidates before the rest, declared order preserved within each group.
37 UniqueFirst
38 };
39
40 /**
41 * @brief Human-readable mapping for @ref CandidateOrder.
42 * @param order The ordering preference.
43 * @return A string view describing the ordering.
44 */
45 2 [[nodiscard]] constexpr std::string_view candidate_order_to_string(CandidateOrder order) noexcept
46 {
47
2/3
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✗ Branch 2 → 5 not taken.
2 switch (order)
48 {
49 1 case CandidateOrder::AsDeclared:
50 1 return "AsDeclared";
51 1 case CandidateOrder::UniqueFirst:
52 1 return "UniqueFirst";
53 }
54 return "Unknown candidate order";
55 }
56
57 /**
58 * @struct ScanProfile
59 * @brief Setup-only control-plane defaults for scan tuning. Value-semantic, not hot-path.
60 * @details Bundles three per-game defaults: the broad string-xref sweep, the cascade candidate-ordering preference,
61 * and a backend deny-list. A profile only supplies defaults: broad string-xref can only widen, and
62 * candidate ordering is applied by building a local reordered span rather than mutating caller-owned
63 * tables. Trivially copyable; hold one per game in static or owner storage and pass it by const reference.
64 * Used only at setup/resolve time, never on a hot path.
65 */
66 struct ScanProfile
67 {
68 /**
69 * @brief Default broad string-xref mode applied when a query leaves
70 * @ref Scanner::StringRefQuery::broad_match at its default.
71 * @details Can only widen coverage, never disable it (see @ref apply_profile).
72 */
73 bool default_broad_string_xref = false;
74
75 /// Cascade candidate-ordering preference. @ref CandidateOrder::AsDeclared preserves existing behaviour.
76 CandidateOrder candidate_order = CandidateOrder::AsDeclared;
77
78 /**
79 * @brief Backend deny-list indexed by @ref Anchors::AnchorKind: a kind is denied when its slot is true.
80 * @details A denied kind fails closed at resolve time (status Failed, value 0); it is never silently replaced
81 * by another backend. Default (all false) denies nothing.
82 */
83 std::array<bool, Anchors::ANCHOR_KIND_COUNT> deny_backend{};
84
85 /**
86 * @brief Whether @p kind is on the deny-list.
87 * @param kind The anchor backend kind.
88 * @return true when the kind is denied.
89 */
90 95 [[nodiscard]] bool is_denied(Anchors::AnchorKind kind) const noexcept
91 {
92 95 const auto index = static_cast<std::size_t>(kind);
93
3/4
✓ Branch 4 → 5 taken 95 times.
✗ Branch 4 → 8 not taken.
✓ Branch 6 → 7 taken 6 times.
✓ Branch 6 → 8 taken 89 times.
190 return index < deny_backend.size() && deny_backend[index];
94 }
95 };
96
97 /**
98 * @brief Returns a copy of @p query with profile defaults filled into any field left at its default.
99 * @details Per-call wins: @c broad_match is turned on only when the query left it false AND the profile defaults it
100 * on. A query that explicitly set @c broad_match = true is never downgraded. Because a bool cannot
101 * distinguish "left default" from "explicitly off", the profile can only ever widen coverage (turn broad
102 * on), never force it off -- which only adds fail-closed coverage and never removes it. Pure, value in /
103 * value out.
104 * @param profile The control-plane defaults.
105 * @param query The per-call query (taken by value, returned modified).
106 * @return The query with the profile's broad default applied.
107 */
108 3 [[nodiscard]] inline Scanner::StringRefQuery apply_profile(const ScanProfile &profile,
109 Scanner::StringRefQuery query) noexcept
110 {
111
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 5 taken 2 times.
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
3 if (!query.broad_match && profile.default_broad_string_xref)
112 {
113 1 query.broad_match = true;
114 }
115 3 return query;
116 }
117
118 /**
119 * @brief Writes candidate indices into @p out in the profile's preferred order.
120 * @details Produces a permutation of @c [0, candidates.size()) rather than mutating the caller's (typically static)
121 * candidate span: a consumer builds a reordered local array from the indices and passes it to a
122 * @c resolve_cascade* entry point. @ref CandidateOrder::AsDeclared writes the identity permutation; @ref
123 * CandidateOrder::UniqueFirst writes the require_unique candidates first (declared order preserved within
124 * each group). Reordering only changes WHICH provably-unique candidate is tried first; it cannot
125 * manufacture a wrong hit, because each candidate is still verified unique-in-scope and in-range at
126 * resolve time, so promoting strict anchors ahead of broad fallbacks is strictly a safety improvement.
127 * @param profile The ordering preference.
128 * @param candidates The candidate cascade.
129 * @param out Destination index buffer; at most @c out.size() indices are written.
130 * @return The number of indices written: @c min(candidates.size(), out.size()).
131 */
132 5 [[nodiscard]] inline std::size_t order_candidates(const ScanProfile &profile,
133 std::span<const Scanner::AddrCandidate> candidates,
134 std::span<std::size_t> out) noexcept
135 {
136
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 5 times.
5 const std::size_t count = (candidates.size() < out.size()) ? candidates.size() : out.size();
137
2/2
✓ Branch 7 → 8 taken 4 times.
✓ Branch 7 → 33 taken 1 time.
5 if (profile.candidate_order == CandidateOrder::UniqueFirst)
138 {
139 4 std::size_t written = 0;
140
5/6
✓ Branch 15 → 16 taken 8 times.
✓ Branch 15 → 18 taken 4 times.
✓ Branch 16 → 17 taken 8 times.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 9 taken 8 times.
✓ Branch 19 → 20 taken 4 times.
12 for (std::size_t i = 0; i < candidates.size() && written < count; ++i)
141 {
142
2/2
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 13 taken 4 times.
8 if (candidates[i].require_unique)
143 {
144 4 out[written++] = i;
145 }
146 }
147
5/6
✓ Branch 27 → 28 taken 8 times.
✗ Branch 27 → 30 not taken.
✓ Branch 28 → 29 taken 4 times.
✓ Branch 28 → 30 taken 4 times.
✓ Branch 31 → 21 taken 4 times.
✓ Branch 31 → 32 taken 4 times.
8 for (std::size_t i = 0; i < candidates.size() && written < count; ++i)
148 {
149
1/2
✓ Branch 22 → 23 taken 4 times.
✗ Branch 22 → 25 not taken.
4 if (!candidates[i].require_unique)
150 {
151 4 out[written++] = i;
152 }
153 }
154 4 return written;
155 }
156
2/2
✓ Branch 36 → 34 taken 3 times.
✓ Branch 36 → 37 taken 1 time.
4 for (std::size_t i = 0; i < count; ++i)
157 {
158 3 out[i] = i;
159 }
160 1 return count;
161 }
162
163 namespace Anchors
164 {
165 /**
166 * @brief Resolves one anchor against a module range, honoring a @ref ScanProfile.
167 * @details Identical to @ref resolve except that the profile is consulted first: a backend on the profile's
168 * deny-list fails closed (status @ref AnchorStatus::Failed, value 0) before any scan, never a silent
169 * substitution; @ref AnchorKind::StringXref inherits the profile's broad-default (widening only); and
170 * cascade-backed anchors (@ref AnchorKind::RipGlobal and @ref AnchorKind::CodeOperand) apply the
171 * profile's candidate-order preference through a local reordered span. For a @ref AnchorKind::Quorum
172 * the profile threads into both sub-anchors, so a denied sub-anchor kind fails the quorum closed and
173 * ordering/broad defaults stay uniform.
174 * @param anchor The anchor declaration.
175 * @param profile The control-plane defaults and deny-list.
176 * @param range Module image to resolve in. Defaults to the host EXE.
177 * @return The resolved value and status.
178 */
179 [[nodiscard]] ResolvedAnchor resolve_with_profile(const Anchor &anchor, const ScanProfile &profile,
180 Memory::ModuleRange range = Memory::host_module_range());
181
182 /**
183 * @brief Resolves a whole anchor table in one pass, honoring a @ref ScanProfile.
184 * @param anchors The declarative anchor table.
185 * @param out Destination, parallel to @p anchors. At most @c out.size() entries are written.
186 * @param profile The control-plane defaults and deny-list.
187 * @param range Module image to resolve in. Defaults to the host EXE.
188 * @return The number of entries written: @c min(anchors.size(), out.size()).
189 */
190 [[nodiscard]] std::size_t resolve_all_with_profile(std::span<const Anchor> anchors,
191 std::span<ResolvedAnchor> out, const ScanProfile &profile,
192 Memory::ModuleRange range = Memory::host_module_range());
193
194 /**
195 * @brief Resolves a whole anchor table concurrently, honoring a @ref ScanProfile.
196 * @details Parallel counterpart to @ref resolve_all_with_profile. Each anchor is resolved by exactly one
197 * worker through @ref resolve_with_profile, and results are copied to @p out in input order. Use the
198 * serial resolver when validator context is order-dependent.
199 * @param anchors The declarative anchor table.
200 * @param out Destination, parallel to @p anchors. At most @c out.size() entries are written.
201 * @param profile The control-plane defaults and deny-list.
202 * @param range Module image to resolve in. Defaults to the host EXE.
203 * @param max_workers Upper bound on concurrent workers. 0 selects std::thread::hardware_concurrency(), clamped
204 * to the entry count. The calling thread participates.
205 * @return The number of entries written: @c min(anchors.size(), out.size()).
206 * @note Setup/control-plane only: spawns threads and allocates. Validators may run concurrently, so validator
207 * functions and their context must be reentrant or externally synchronized.
208 */
209 [[nodiscard]] std::size_t resolve_all_with_profile_parallel(
210 std::span<const Anchor> anchors, std::span<ResolvedAnchor> out, const ScanProfile &profile,
211 Memory::ModuleRange range = Memory::host_module_range(), std::size_t max_workers = 0);
212 } // namespace Anchors
213 } // namespace DetourModKit
214
215 #endif // DETOURMODKIT_PROFILE_HPP
216