include/DetourModKit/hook_manager.hpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #ifndef DETOURMODKIT_HOOK_MANAGER_HPP | ||
| 2 | #define DETOURMODKIT_HOOK_MANAGER_HPP | ||
| 3 | |||
| 4 | #include "DetourModKit/format.hpp" | ||
| 5 | #include "DetourModKit/logger.hpp" | ||
| 6 | #include "DetourModKit/scanner.hpp" | ||
| 7 | #include "DetourModKit/srw_shared_mutex.hpp" | ||
| 8 | |||
| 9 | #include "safetyhook.hpp" | ||
| 10 | |||
| 11 | #include <atomic> | ||
| 12 | #include <concepts> | ||
| 13 | #include <cstdint> | ||
| 14 | #include <expected> | ||
| 15 | #include <format> | ||
| 16 | #include <functional> | ||
| 17 | #include <memory> | ||
| 18 | #include <mutex> | ||
| 19 | #include <optional> | ||
| 20 | #include <shared_mutex> | ||
| 21 | #include <span> | ||
| 22 | #include <string> | ||
| 23 | #include <string_view> | ||
| 24 | #include <type_traits> | ||
| 25 | #include <unordered_map> | ||
| 26 | #include <utility> | ||
| 27 | #include <vector> | ||
| 28 | |||
| 29 | namespace DetourModKit | ||
| 30 | { | ||
| 31 | namespace detail | ||
| 32 | { | ||
| 33 | /** | ||
| 34 | * @brief Transparent hash functor for heterogeneous lookup in string-keyed maps. | ||
| 35 | * @details Allows std::string_view lookups without constructing a temporary std::string. | ||
| 36 | */ | ||
| 37 | struct TransparentStringHash | ||
| 38 | { | ||
| 39 | using is_transparent = void; | ||
| 40 | 30554 | size_t operator()(std::string_view sv) const noexcept { return std::hash<std::string_view>{}(sv); } | |
| 41 | }; | ||
| 42 | } // namespace detail | ||
| 43 | |||
| 44 | /** | ||
| 45 | * @enum HookType | ||
| 46 | * @brief Enumeration of supported hook types, corresponding to SafetyHook capabilities. | ||
| 47 | */ | ||
| 48 | enum class HookType | ||
| 49 | { | ||
| 50 | Inline, | ||
| 51 | Mid, | ||
| 52 | Vmt | ||
| 53 | }; | ||
| 54 | |||
| 55 | /** | ||
| 56 | * @enum HookStatus | ||
| 57 | * @brief Represents the current operational status of a managed hook. | ||
| 58 | */ | ||
| 59 | enum class HookStatus | ||
| 60 | { | ||
| 61 | Active, | ||
| 62 | Disabled, | ||
| 63 | Enabling, | ||
| 64 | Disabling | ||
| 65 | }; | ||
| 66 | |||
| 67 | /** | ||
| 68 | * @enum InlineProloguePolicy | ||
| 69 | * @brief Escalation policy for the inline/mid hook prologue pre-flight. | ||
| 70 | * @details Controls what happens when the target's first opcode is a leading E8 (call rel32) or a breakpoint byte | ||
| 71 | * (0xCC int3 / 0xCD int n) at create time. A leading call means the inline hook's 5-byte E9 patch would | ||
| 72 | * steal a relative call whose displacement was computed from the original site, so the relocated | ||
| 73 | * trampoline copy can dispatch the call to the wrong absolute target; a leading int3 means the slot is | ||
| 74 | * already a breakpoint -- a foreign hook's stub, a patched byte, or alignment padding -- not a real | ||
| 75 | * function body. @ref Warn logs and installs anyway (the default, preserving historical behaviour); @ref | ||
| 76 | * Fail refuses the create with @ref HookError::TargetPrologueUnsafe. | ||
| 77 | */ | ||
| 78 | enum class InlineProloguePolicy | ||
| 79 | { | ||
| 80 | Warn, | ||
| 81 | Fail | ||
| 82 | }; | ||
| 83 | |||
| 84 | /** | ||
| 85 | * @enum HookError | ||
| 86 | * @brief Error codes for hook creation/operation failures. | ||
| 87 | */ | ||
| 88 | enum class HookError | ||
| 89 | { | ||
| 90 | AllocatorNotAvailable, | ||
| 91 | InvalidTargetAddress, | ||
| 92 | InvalidDetourFunction, | ||
| 93 | InvalidTrampolinePointer, | ||
| 94 | HookAlreadyExists, | ||
| 95 | HookNotFound, | ||
| 96 | ShutdownInProgress, | ||
| 97 | SafetyHookError, | ||
| 98 | EnableFailed, | ||
| 99 | DisableFailed, | ||
| 100 | InvalidHookState, | ||
| 101 | InvalidObject, | ||
| 102 | VmtHookNotFound, | ||
| 103 | MethodAlreadyHooked, | ||
| 104 | MethodNotFound, | ||
| 105 | TargetAlreadyHookedInProcess, | ||
| 106 | ReentrantCallRejected, | ||
| 107 | TargetPrologueUnsafe, | ||
| 108 | UnknownError | ||
| 109 | }; | ||
| 110 | |||
| 111 | /** | ||
| 112 | * @struct HookConfig | ||
| 113 | * @brief Configuration options used during the creation of a new hook. | ||
| 114 | */ | ||
| 115 | struct HookConfig | ||
| 116 | { | ||
| 117 | bool auto_enable = true; | ||
| 118 | |||
| 119 | /** | ||
| 120 | * @brief Refuses hooks when the target already appears hooked. | ||
| 121 | * @details Applies to inline and mid hooks. The pre-flight first checks this HookManager's registry for an | ||
| 122 | * exact same-address hook, then falls back to a foreign JMP prologue heuristic. With the default | ||
| 123 | * (false), a warning is logged and bulk teardown unwinds managed layers newest-first. | ||
| 124 | */ | ||
| 125 | bool fail_if_already_hooked = false; | ||
| 126 | |||
| 127 | /** | ||
| 128 | * @brief Escalation policy when the target prologue leads with a call (E8) or a breakpoint (0xCC/0xCD) byte. | ||
| 129 | * @details A leading 0xE8 call rel32 or a 0xCC/0xCD breakpoint at the hook site is the risk this surfaces: | ||
| 130 | * hooking a prologue that is itself a relative call steals a displacement that was relative to the | ||
| 131 | * original address (the relocated trampoline copy can then call the wrong absolute target), and | ||
| 132 | * hooking an int3 byte means the entry is already a breakpoint stub (a foreign hook or a patched / | ||
| 133 | * padding byte), not a real function body. The pre-flight decodes the first byte under a fault guard | ||
| 134 | * and, on a match, either logs a warning and installs anyway (@ref InlineProloguePolicy::Warn, the | ||
| 135 | * default, preserving current behaviour) or refuses with @ref HookError::TargetPrologueUnsafe (@ref | ||
| 136 | * InlineProloguePolicy::Fail). Applies to inline and mid hooks; both patch the same prologue. A rare | ||
| 137 | * legitimate function whose true first instruction is a call thunk will warn (harmless) or, under | ||
| 138 | * Fail, be refused -- opt into Fail only for targets known to be ordinary function bodies. | ||
| 139 | */ | ||
| 140 | InlineProloguePolicy prologue_policy = InlineProloguePolicy::Warn; | ||
| 141 | }; | ||
| 142 | |||
| 143 | /** | ||
| 144 | * @struct VmtHookConfig | ||
| 145 | * @brief Configuration options used during VMT hook creation and apply, symmetric with @ref HookConfig. | ||
| 146 | * @details Mirrors @ref HookConfig for VMT hooks so the inline and VMT code paths expose the same operational | ||
| 147 | * surface. The defaults are chosen to preserve the historical single-argument API's behavior exactly | ||
| 148 | * (no pre-flight checks; pre-existing failures such as a null object, a duplicate name, shutdown in | ||
| 149 | * progress, or a SafetyHook error still apply), so existing call sites that build a default config | ||
| 150 | * compile and run unchanged. | ||
| 151 | */ | ||
| 152 | struct VmtHookConfig | ||
| 153 | { | ||
| 154 | /** | ||
| 155 | * @brief When true, refuse to clone an object whose vptr already points at a VMT cloned by this HookManager. | ||
| 156 | * @details SafetyHook::VmtHook::create replaces the object's vptr with a pointer into a freshly-allocated | ||
| 157 | * cloned vtable. If a second call later clones the same object, the second create reads the first | ||
| 158 | * clone as if it were the original vtable: the first mod's hooked methods are now baked into the | ||
| 159 | * second mod's "original" and a third call layered on top of the second sees a third level of | ||
| 160 | * redirection. This is the silent "double hook" failure mode the inline hook's | ||
| 161 | * @ref HookConfig::fail_if_already_hooked guards against; VMT cloning is the same shape of risk and | ||
| 162 | * gets the same knob. Default false preserves the legacy permissive behavior. | ||
| 163 | */ | ||
| 164 | bool fail_if_already_hooked = false; | ||
| 165 | |||
| 166 | /** | ||
| 167 | * @brief When true, pre-flight-decode the first byte of the original vtable slot and refuse to clone when | ||
| 168 | * the byte is an int3/int padding breakpoint or a same-module jump stub. | ||
| 169 | * @details A VMT slot whose first byte is 0xCC is an alignment pad or permanent breakpoint, not a real | ||
| 170 | * function. Replacing it and dispatching through it yields __debugbreak under the consumer's | ||
| 171 | * debugger and an instant crash in shipping builds. A VMT slot whose first instruction is `jmp | ||
| 172 | * rel8/rel32` to a target inside the same module is a jump stub (e.g. an incremental-link ILT | ||
| 173 | * entry), not a function body; MSVC adjustor thunks for multiple-inheritance vtables start with the | ||
| 174 | * this-adjust instruction, so they pass. The check is intentionally conservative: real functions | ||
| 175 | * (any other first byte, or a tail-call `mov reg,reg; jmp <out-of-module>`) pass. Known false | ||
| 176 | * positive: consumer binaries built with /INCREMENTAL route every function through an ILT jump | ||
| 177 | * stub, which this check rejects. The default is false to preserve the historical no-pre-flight | ||
| 178 | * behavior; opt in for the safety net on mods that exclusively target well-formed C++ vtables. | ||
| 179 | */ | ||
| 180 | bool fail_on_non_function_pointer = false; | ||
| 181 | }; | ||
| 182 | |||
| 183 | /** | ||
| 184 | * @class Hook | ||
| 185 | * @brief Abstract base class for managed hooks. | ||
| 186 | * @details Defines a common interface for interacting with different types of hooks managed by the HookManager. | ||
| 187 | * Implements the Template Method pattern for enable/disable state management. | ||
| 188 | */ | ||
| 189 | class Hook | ||
| 190 | { | ||
| 191 | public: | ||
| 192 | 120 | virtual ~Hook() noexcept = default; | |
| 193 | |||
| 194 | 31 | [[nodiscard]] const std::string &get_name() const noexcept { return m_name; } | |
| 195 | 869 | [[nodiscard]] HookType get_type() const noexcept { return m_type; } | |
| 196 | 39 | [[nodiscard]] uintptr_t get_target_address() const noexcept { return m_target_address; } | |
| 197 | 30568 | [[nodiscard]] HookStatus get_status() const noexcept { return m_status.load(std::memory_order_acquire); } | |
| 198 | |||
| 199 | /** | ||
| 200 | * @brief Enables the hook. | ||
| 201 | * @return Success if the hook was enabled or already active. On failure, the HookError indicates the reason | ||
| 202 | * (SafetyHookError, EnableFailed, InvalidHookState). | ||
| 203 | * @note Uses atomic CAS for lock-free status transitions. Thread-safe without requiring external | ||
| 204 | * synchronization. Uses an intermediate Enabling state to prevent other threads from observing a | ||
| 205 | * speculative terminal state while the SafetyHook enable call is in progress. | ||
| 206 | */ | ||
| 207 | 417 | [[nodiscard]] std::expected<void, HookError> enable() | |
| 208 | { | ||
| 209 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 417 times.
|
417 | if (!is_impl_valid()) |
| 210 | ✗ | return std::unexpected(HookError::SafetyHookError); | |
| 211 | |||
| 212 | 417 | HookStatus expected = HookStatus::Disabled; | |
| 213 |
2/2✓ Branch 9 → 10 taken 355 times.
✓ Branch 9 → 18 taken 62 times.
|
417 | if (!m_status.compare_exchange_strong(expected, HookStatus::Enabling, std::memory_order_acq_rel)) |
| 214 | { | ||
| 215 |
2/2✓ Branch 10 → 11 taken 18 times.
✓ Branch 10 → 14 taken 337 times.
|
355 | if (expected == HookStatus::Active) |
| 216 | 18 | return {}; | |
| 217 | 337 | return std::unexpected(HookError::InvalidHookState); | |
| 218 | } | ||
| 219 | |||
| 220 |
2/4✓ Branch 18 → 19 taken 62 times.
✗ Branch 18 → 31 not taken.
✓ Branch 19 → 20 taken 62 times.
✗ Branch 19 → 24 not taken.
|
62 | if (do_enable()) |
| 221 | { | ||
| 222 | 62 | m_status.store(HookStatus::Active, std::memory_order_release); | |
| 223 | 62 | return {}; | |
| 224 | } | ||
| 225 | |||
| 226 | ✗ | m_status.store(HookStatus::Disabled, std::memory_order_release); | |
| 227 | ✗ | return std::unexpected(HookError::EnableFailed); | |
| 228 | } | ||
| 229 | |||
| 230 | /** | ||
| 231 | * @brief Disables the hook. | ||
| 232 | * @return Success if the hook was disabled or already disabled. On failure, the HookError indicates the reason | ||
| 233 | * (SafetyHookError, DisableFailed, InvalidHookState). | ||
| 234 | * @note Uses atomic CAS for lock-free status transitions. Thread-safe without requiring external | ||
| 235 | * synchronization. Uses an intermediate Disabling state to prevent other threads from observing a | ||
| 236 | * speculative terminal state while the SafetyHook disable call is in progress. | ||
| 237 | */ | ||
| 238 | 543 | [[nodiscard]] std::expected<void, HookError> disable() | |
| 239 | { | ||
| 240 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 543 times.
|
543 | if (!is_impl_valid()) |
| 241 | ✗ | return std::unexpected(HookError::SafetyHookError); | |
| 242 | |||
| 243 | 543 | HookStatus expected = HookStatus::Active; | |
| 244 |
2/2✓ Branch 9 → 10 taken 367 times.
✓ Branch 9 → 18 taken 176 times.
|
543 | if (!m_status.compare_exchange_strong(expected, HookStatus::Disabling, std::memory_order_acq_rel)) |
| 245 | { | ||
| 246 |
2/2✓ Branch 10 → 11 taken 27 times.
✓ Branch 10 → 14 taken 340 times.
|
367 | if (expected == HookStatus::Disabled) |
| 247 | 27 | return {}; | |
| 248 | 340 | return std::unexpected(HookError::InvalidHookState); | |
| 249 | } | ||
| 250 | |||
| 251 |
2/4✓ Branch 18 → 19 taken 176 times.
✗ Branch 18 → 31 not taken.
✓ Branch 19 → 20 taken 176 times.
✗ Branch 19 → 24 not taken.
|
176 | if (do_disable()) |
| 252 | { | ||
| 253 | 176 | m_status.store(HookStatus::Disabled, std::memory_order_release); | |
| 254 | 176 | return {}; | |
| 255 | } | ||
| 256 | |||
| 257 | ✗ | m_status.store(HookStatus::Active, std::memory_order_release); | |
| 258 | ✗ | return std::unexpected(HookError::DisableFailed); | |
| 259 | } | ||
| 260 | |||
| 261 | [[nodiscard]] bool is_enabled() const noexcept | ||
| 262 | { | ||
| 263 | return m_status.load(std::memory_order_acquire) == HookStatus::Active; | ||
| 264 | } | ||
| 265 | |||
| 266 | 680 | [[nodiscard]] static constexpr std::string_view status_to_string(HookStatus status) noexcept | |
| 267 | { | ||
| 268 |
5/5✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 4 taken 1 time.
✓ Branch 2 → 5 taken 347 times.
✓ Branch 2 → 6 taken 330 times.
✓ Branch 2 → 7 taken 1 time.
|
680 | switch (status) |
| 269 | { | ||
| 270 | 1 | case HookStatus::Active: | |
| 271 | 1 | return "Active"; | |
| 272 | 1 | case HookStatus::Disabled: | |
| 273 | 1 | return "Disabled"; | |
| 274 | 347 | case HookStatus::Enabling: | |
| 275 | 347 | return "Enabling"; | |
| 276 | 330 | case HookStatus::Disabling: | |
| 277 | 330 | return "Disabling"; | |
| 278 | 1 | default: | |
| 279 | 1 | return "Unknown"; | |
| 280 | } | ||
| 281 | } | ||
| 282 | |||
| 283 | 33 | [[nodiscard]] static constexpr std::string_view error_to_string(HookError error) noexcept | |
| 284 | { | ||
| 285 |
16/20✓ Branch 2 → 3 taken 3 times.
✓ Branch 2 → 4 taken 2 times.
✓ Branch 2 → 5 taken 2 times.
✓ Branch 2 → 6 taken 2 times.
✓ Branch 2 → 7 taken 2 times.
✓ Branch 2 → 8 taken 4 times.
✓ Branch 2 → 9 taken 4 times.
✓ Branch 2 → 10 taken 2 times.
✓ Branch 2 → 11 taken 1 time.
✓ Branch 2 → 12 taken 1 time.
✓ Branch 2 → 13 taken 1 time.
✓ Branch 2 → 14 taken 2 times.
✓ Branch 2 → 15 taken 2 times.
✓ Branch 2 → 16 taken 2 times.
✓ Branch 2 → 17 taken 1 time.
✗ Branch 2 → 18 not taken.
✗ Branch 2 → 19 not taken.
✗ Branch 2 → 20 not taken.
✓ Branch 2 → 21 taken 2 times.
✗ Branch 2 → 22 not taken.
|
33 | switch (error) |
| 286 | { | ||
| 287 | 3 | case HookError::AllocatorNotAvailable: | |
| 288 | 3 | return "Allocator not available"; | |
| 289 | 2 | case HookError::InvalidTargetAddress: | |
| 290 | 2 | return "Invalid target address"; | |
| 291 | 2 | case HookError::InvalidDetourFunction: | |
| 292 | 2 | return "Invalid detour function"; | |
| 293 | 2 | case HookError::InvalidTrampolinePointer: | |
| 294 | 2 | return "Invalid trampoline pointer"; | |
| 295 | 2 | case HookError::HookAlreadyExists: | |
| 296 | 2 | return "Hook already exists"; | |
| 297 | 4 | case HookError::HookNotFound: | |
| 298 | 4 | return "Hook not found"; | |
| 299 | 4 | case HookError::ShutdownInProgress: | |
| 300 | 4 | return "Shutdown in progress"; | |
| 301 | 2 | case HookError::SafetyHookError: | |
| 302 | 2 | return "SafetyHook error"; | |
| 303 | 1 | case HookError::EnableFailed: | |
| 304 | 1 | return "Hook enable failed"; | |
| 305 | 1 | case HookError::DisableFailed: | |
| 306 | 1 | return "Hook disable failed"; | |
| 307 | 1 | case HookError::InvalidHookState: | |
| 308 | 1 | return "Hook is in a transitional state"; | |
| 309 | 2 | case HookError::InvalidObject: | |
| 310 | 2 | return "Invalid object pointer"; | |
| 311 | 2 | case HookError::VmtHookNotFound: | |
| 312 | 2 | return "VMT hook not found"; | |
| 313 | 2 | case HookError::MethodAlreadyHooked: | |
| 314 | 2 | return "VMT method already hooked"; | |
| 315 | 1 | case HookError::MethodNotFound: | |
| 316 | 1 | return "VMT method hook not found"; | |
| 317 | ✗ | case HookError::TargetAlreadyHookedInProcess: | |
| 318 | ✗ | return "Target address is already hooked in this process"; | |
| 319 | ✗ | case HookError::ReentrantCallRejected: | |
| 320 | ✗ | return "Mutator called reentrantly from within a with_* callback"; | |
| 321 | ✗ | case HookError::TargetPrologueUnsafe: | |
| 322 | ✗ | return "Target prologue leads with a call (E8) or breakpoint (0xCC/0xCD) byte"; | |
| 323 | 2 | case HookError::UnknownError: | |
| 324 | 2 | return "Unknown error"; | |
| 325 | ✗ | default: | |
| 326 | ✗ | return "Invalid error code"; | |
| 327 | } | ||
| 328 | } | ||
| 329 | |||
| 330 | protected: | ||
| 331 | std::string m_name; | ||
| 332 | HookType m_type; | ||
| 333 | uintptr_t m_target_address; | ||
| 334 | std::atomic<HookStatus> m_status; | ||
| 335 | |||
| 336 | 120 | Hook(std::string name, HookType type, uintptr_t target_address, HookStatus initial_status) | |
| 337 | 240 | : m_name(std::move(name)), m_type(type), m_target_address(target_address), m_status(initial_status) | |
| 338 | { | ||
| 339 | 120 | } | |
| 340 | |||
| 341 | virtual bool is_impl_valid() const noexcept = 0; | ||
| 342 | virtual bool do_enable() = 0; | ||
| 343 | virtual bool do_disable() = 0; | ||
| 344 | |||
| 345 | Hook(const Hook &) = delete; | ||
| 346 | Hook &operator=(const Hook &) = delete; | ||
| 347 | Hook(Hook &&) = delete; | ||
| 348 | Hook &operator=(Hook &&) = delete; | ||
| 349 | }; | ||
| 350 | |||
| 351 | namespace detail | ||
| 352 | { | ||
| 353 | /// Satisfied only by a pointer-to-function type; the valid cast target for InlineHook::get_original. | ||
| 354 | template <typename T> | ||
| 355 | concept FunctionPointer = std::is_pointer_v<T> && std::is_function_v<std::remove_pointer_t<T>>; | ||
| 356 | } // namespace detail | ||
| 357 | |||
| 358 | /** | ||
| 359 | * @class InlineHook | ||
| 360 | * @brief Represents a managed inline hook, wrapping a SafetyHook::InlineHook object. | ||
| 361 | */ | ||
| 362 | class InlineHook : public Hook | ||
| 363 | { | ||
| 364 | public: | ||
| 365 | 95 | InlineHook(std::string name, uintptr_t target_address, safetyhook::InlineHook hook_obj, | |
| 366 | HookStatus initial_status) | ||
| 367 | 190 | : Hook(std::move(name), HookType::Inline, target_address, initial_status), | |
| 368 | 285 | m_safetyhook_impl(std::move(hook_obj)) | |
| 369 | { | ||
| 370 | 95 | } | |
| 371 | |||
| 372 | /** | ||
| 373 | * @brief Retrieves the trampoline to call the original function. | ||
| 374 | * @tparam T The function pointer type of the original function. | ||
| 375 | * @return A function pointer of type T to the original function's trampoline. | ||
| 376 | */ | ||
| 377 | 2 | template <detail::FunctionPointer T> [[nodiscard]] T get_original() const noexcept | |
| 378 | { | ||
| 379 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
|
2 | return m_safetyhook_impl ? m_safetyhook_impl.original<T>() : nullptr; |
| 380 | } | ||
| 381 | |||
| 382 | protected: | ||
| 383 | 927 | bool is_impl_valid() const noexcept override { return static_cast<bool>(m_safetyhook_impl); } | |
| 384 | 59 | bool do_enable() override | |
| 385 | { | ||
| 386 |
1/2✓ Branch 2 → 3 taken 59 times.
✗ Branch 2 → 6 not taken.
|
59 | auto result = m_safetyhook_impl.enable(); |
| 387 | 118 | return result.has_value(); | |
| 388 | } | ||
| 389 | 150 | bool do_disable() override | |
| 390 | { | ||
| 391 |
1/2✓ Branch 2 → 3 taken 150 times.
✗ Branch 2 → 6 not taken.
|
150 | auto result = m_safetyhook_impl.disable(); |
| 392 | 300 | return result.has_value(); | |
| 393 | } | ||
| 394 | |||
| 395 | private: | ||
| 396 | safetyhook::InlineHook m_safetyhook_impl; | ||
| 397 | }; | ||
| 398 | |||
| 399 | /** | ||
| 400 | * @class MidHook | ||
| 401 | * @brief Represents a managed mid-function hook, wrapping a SafetyHook::MidHook object. | ||
| 402 | */ | ||
| 403 | class MidHook : public Hook | ||
| 404 | { | ||
| 405 | public: | ||
| 406 | 25 | MidHook(std::string name, uintptr_t target_address, safetyhook::MidHook hook_obj, HookStatus initial_status) | |
| 407 | 50 | : Hook(std::move(name), HookType::Mid, target_address, initial_status), | |
| 408 | 75 | m_safetyhook_impl(std::move(hook_obj)) | |
| 409 | { | ||
| 410 | 25 | } | |
| 411 | |||
| 412 | /** | ||
| 413 | * @brief Gets the destination function of this mid-hook. | ||
| 414 | * @return safetyhook::MidHookFn The function pointer to the detour. | ||
| 415 | */ | ||
| 416 | 1 | [[nodiscard]] safetyhook::MidHookFn get_destination() const noexcept | |
| 417 | { | ||
| 418 |
1/2✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
|
1 | return m_safetyhook_impl ? m_safetyhook_impl.destination() : nullptr; |
| 419 | } | ||
| 420 | |||
| 421 | protected: | ||
| 422 | 32 | bool is_impl_valid() const noexcept override { return static_cast<bool>(m_safetyhook_impl); } | |
| 423 | 3 | bool do_enable() override | |
| 424 | { | ||
| 425 |
1/2✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 6 not taken.
|
3 | auto result = m_safetyhook_impl.enable(); |
| 426 | 6 | return result.has_value(); | |
| 427 | } | ||
| 428 | 26 | bool do_disable() override | |
| 429 | { | ||
| 430 |
1/2✓ Branch 2 → 3 taken 26 times.
✗ Branch 2 → 6 not taken.
|
26 | auto result = m_safetyhook_impl.disable(); |
| 431 | 52 | return result.has_value(); | |
| 432 | } | ||
| 433 | |||
| 434 | private: | ||
| 435 | safetyhook::MidHook m_safetyhook_impl; | ||
| 436 | }; | ||
| 437 | |||
| 438 | namespace detail | ||
| 439 | { | ||
| 440 | /** | ||
| 441 | * @class VmtHookEntry | ||
| 442 | * @brief Manages a VMT hook for a single object class, wrapping SafetyHook's VmtHook. | ||
| 443 | * @details Owns the cloned vtable and tracks individual method hooks by vtable index. VMT hooks operate at the | ||
| 444 | * object level by replacing the vptr with a cloned vtable. Individual methods are hooked by index | ||
| 445 | * within the cloned table. Does not support enable/disable toggling (SafetyHook VmHook limitation). | ||
| 446 | * Internal: held only inside @ref VmtHookMap and not part of the public API. | ||
| 447 | */ | ||
| 448 | class VmtHookEntry | ||
| 449 | { | ||
| 450 | public: | ||
| 451 | /** | ||
| 452 | * @brief Constructs an entry that takes ownership of a SafetyHook VMT hook. | ||
| 453 | * @param name The registered hook name. | ||
| 454 | * @param vmt_hook The VMT hook to own. | ||
| 455 | * @param new_vptr_base The vptr value SafetyHook installed on the seeded object, i.e. `&m_new_vmt[1]`. | ||
| 456 | * Stored so subsequent apply_vmt_hook calls can detect "object is already on this | ||
| 457 | * clone" without touching the private SafetyHook layout. Zero is reserved for "not | ||
| 458 | * recorded" and never matches a real vptr. | ||
| 459 | */ | ||
| 460 | 33 | VmtHookEntry(std::string name, safetyhook::VmtHook vmt_hook, std::uintptr_t new_vptr_base) | |
| 461 | 99 | : m_name(std::move(name)), m_vmt_hook(std::move(vmt_hook)), m_cloned_vptr_base(new_vptr_base) | |
| 462 | { | ||
| 463 | 33 | } | |
| 464 | |||
| 465 | /// Returns the registered hook name. | ||
| 466 | 4 | [[nodiscard]] const std::string &get_name() const noexcept { return m_name; } | |
| 467 | |||
| 468 | /// Returns the underlying SafetyHook VMT hook. | ||
| 469 | 10 | [[nodiscard]] safetyhook::VmtHook &vmt_hook() noexcept { return m_vmt_hook; } | |
| 470 | |||
| 471 | /// Returns the vptr this entry installed on its seed object, or 0 if unknown. | ||
| 472 | 8 | [[nodiscard]] std::uintptr_t cloned_vptr_base() const noexcept { return m_cloned_vptr_base; } | |
| 473 | |||
| 474 | /// Returns true if a method at the given vtable index is hooked. | ||
| 475 | 8 | [[nodiscard]] bool has_method_hook(size_t index) const noexcept | |
| 476 | { | ||
| 477 | 8 | return m_method_hooks.find(index) != m_method_hooks.end(); | |
| 478 | } | ||
| 479 | |||
| 480 | /** | ||
| 481 | * @brief Returns the method hook installed at a vtable index. | ||
| 482 | * @param index The vtable index to look up. | ||
| 483 | * @return Pointer to the method hook, or nullptr if none is installed. | ||
| 484 | */ | ||
| 485 | 7 | [[nodiscard]] safetyhook::VmHook *get_method_hook(size_t index) | |
| 486 | { | ||
| 487 |
1/2✓ Branch 2 → 3 taken 7 times.
✗ Branch 2 → 12 not taken.
|
7 | auto it = m_method_hooks.find(index); |
| 488 |
2/2✓ Branch 5 → 6 taken 6 times.
✓ Branch 5 → 8 taken 1 time.
|
7 | return it != m_method_hooks.end() ? &it->second : nullptr; |
| 489 | } | ||
| 490 | |||
| 491 | /** | ||
| 492 | * @brief Installs a method hook at a vtable index. | ||
| 493 | * @param index The vtable index being hooked. | ||
| 494 | * @param hook The method hook to own. | ||
| 495 | */ | ||
| 496 | 7 | void add_method_hook(size_t index, safetyhook::VmHook hook) | |
| 497 | { | ||
| 498 |
1/2✓ Branch 4 → 5 taken 7 times.
✗ Branch 4 → 6 not taken.
|
14 | m_method_hooks.emplace(index, std::move(hook)); |
| 499 | 7 | } | |
| 500 | |||
| 501 | /** | ||
| 502 | * @brief Removes the method hook at a vtable index. | ||
| 503 | * @param index The vtable index to clear. | ||
| 504 | * @return true if a hook was removed, false if none was installed. | ||
| 505 | */ | ||
| 506 | 2 | [[nodiscard]] bool remove_method_hook(size_t index) { return m_method_hooks.erase(index) > 0; } | |
| 507 | |||
| 508 | VmtHookEntry(const VmtHookEntry &) = delete; | ||
| 509 | VmtHookEntry &operator=(const VmtHookEntry &) = delete; | ||
| 510 | VmtHookEntry(VmtHookEntry &&) = default; | ||
| 511 | VmtHookEntry &operator=(VmtHookEntry &&) = default; | ||
| 512 | |||
| 513 | private: | ||
| 514 | std::string m_name; | ||
| 515 | safetyhook::VmtHook m_vmt_hook; | ||
| 516 | std::uintptr_t m_cloned_vptr_base{0}; | ||
| 517 | std::unordered_map<size_t, safetyhook::VmHook> m_method_hooks; | ||
| 518 | }; | ||
| 519 | |||
| 520 | /** | ||
| 521 | * @brief Container type for the inline / mid hook registry, keyed by hook name. | ||
| 522 | * @details Centralized once so every site that references this exact instantiation sees identical template | ||
| 523 | * arguments. | ||
| 524 | */ | ||
| 525 | using HookMap = std::unordered_map<std::string, std::unique_ptr<Hook>, TransparentStringHash, std::equal_to<>>; | ||
| 526 | |||
| 527 | /** | ||
| 528 | * @brief Container type for the VMT hook registry, keyed by hook name. | ||
| 529 | * @details Centralized once so every site that references this exact instantiation sees identical template | ||
| 530 | * arguments. | ||
| 531 | */ | ||
| 532 | using VmtHookMap = std::unordered_map<std::string, VmtHookEntry, TransparentStringHash, std::equal_to<>>; | ||
| 533 | } // namespace detail | ||
| 534 | |||
| 535 | /** | ||
| 536 | * @class HookManager | ||
| 537 | * @brief Manages the lifecycle of all hooks (Inline, Mid, and VMT) using SafetyHook. | ||
| 538 | * @details Provides a centralized API for creating, removing, enabling, and disabling hooks. Thread-safe for all | ||
| 539 | * public methods. Uses std::expected for explicit error handling. | ||
| 540 | * @note Lock ordering: 1. m_mutator_gate (shared or exclusive) then 2. m_hooks_mutex (shared or exclusive). | ||
| 541 | * Mutators (create_*_hook, enable, disable, remove) acquire shared m_mutator_gate first, then shared or | ||
| 542 | * exclusive m_hooks_mutex. Shutdown and remove_all_hooks acquire exclusive m_mutator_gate first to block | ||
| 543 | * new mutators, then proceed with two-phase cleanup. | ||
| 544 | */ | ||
| 545 | class HookManager | ||
| 546 | { | ||
| 547 | public: | ||
| 548 | /** | ||
| 549 | * @brief Provides access to the singleton instance of the HookManager. | ||
| 550 | * @return HookManager& Reference to the global HookManager instance. | ||
| 551 | */ | ||
| 552 | static HookManager &get_instance(); | ||
| 553 | |||
| 554 | ~HookManager() noexcept; | ||
| 555 | |||
| 556 | /** | ||
| 557 | * @brief Explicitly shuts down the HookManager, removing all hooks without logging. | ||
| 558 | * @details This method is safe to call during shutdown when Logger may be destroyed. It removes all hooks | ||
| 559 | * without attempting to log, preventing use-after-free. The shutdown flag is reset after hooks are | ||
| 560 | * cleared, allowing subsequent hook creation for hot-reload scenarios. The destructor becomes a no-op | ||
| 561 | * only while the flag is set during the shutdown operation itself. | ||
| 562 | * @note Two-phase teardown / quiesce contract: acquires the mutator gate exclusively to block new mutators, | ||
| 563 | * disables all hooks first (shared registry lock), then clears the maps under the exclusive lock, both | ||
| 564 | * phases newest-first so layered hooks unwind onto still-valid bytes. SafetyHook relocates a thread | ||
| 565 | * caught in the patched prologue but cannot drain a thread already inside the detour or trampoline body, | ||
| 566 | * so the caller must quiesce the hooked functions before shutdown to close that residual window. Do not | ||
| 567 | * call this (or any mutator) from within a with_* / try_with_* callback; defer the teardown until the | ||
| 568 | * callback returns (the reentrancy guard fails such calls closed). | ||
| 569 | */ | ||
| 570 | void shutdown() noexcept; | ||
| 571 | |||
| 572 | // Non-copyable, non-movable (mutex member) | ||
| 573 | HookManager(const HookManager &) = delete; | ||
| 574 | HookManager &operator=(const HookManager &) = delete; | ||
| 575 | HookManager(HookManager &&) = delete; | ||
| 576 | HookManager &operator=(HookManager &&) = delete; | ||
| 577 | |||
| 578 | /** | ||
| 579 | * @brief Creates an inline hook at a specific target memory address. | ||
| 580 | * @param name A unique, descriptive name for the hook. | ||
| 581 | * @param target_address The memory address of the function to hook. | ||
| 582 | * @param detour_function Pointer to the detour function. | ||
| 583 | * @param original_trampoline Output pointer to receive trampoline address. | ||
| 584 | * @param config Optional configuration settings for the hook. | ||
| 585 | * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise. | ||
| 586 | */ | ||
| 587 | [[nodiscard]] std::expected<std::string, HookError> | ||
| 588 | create_inline_hook(std::string_view name, uintptr_t target_address, void *detour_function, | ||
| 589 | void **original_trampoline, const HookConfig &config = HookConfig()); | ||
| 590 | |||
| 591 | /** | ||
| 592 | * @brief Creates an inline hook by finding target address via AOB scan. | ||
| 593 | * @param name A unique, descriptive name for the hook. | ||
| 594 | * @param module_base Base address of the memory module to scan. | ||
| 595 | * @param module_size Size of the memory module to scan. | ||
| 596 | * @param aob_pattern_str The AOB pattern string. | ||
| 597 | * @param aob_offset Offset to add to the found pattern's address. | ||
| 598 | * @param detour_function Pointer to the detour function. | ||
| 599 | * @param original_trampoline Output pointer to store trampoline address. | ||
| 600 | * @param config Optional configuration settings for the hook. | ||
| 601 | * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise. | ||
| 602 | * @note The AOB scan over [module_base, module_base + module_size) is page-filtered: it walks VirtualQuery | ||
| 603 | * and skips guard, no-access, and non-readable pages, so passing a full SizeOfImage span is safe even | ||
| 604 | * when the image contains a guard or no-access section -- unlike the raw Scanner::find_pattern | ||
| 605 | * overloads, which read the span unconditionally and fault the host on an unreadable byte. A signature | ||
| 606 | * straddling a protection split inside the image is still found, and @p aob_offset is applied to the | ||
| 607 | * located address. For code that lives outside any mapped module (packed payloads unpacked into | ||
| 608 | * anonymous pages), resolve the address with the whole-process Scanner sweeps and call | ||
| 609 | * create_inline_hook with the result instead. | ||
| 610 | */ | ||
| 611 | [[nodiscard]] std::expected<std::string, HookError> | ||
| 612 | create_inline_hook_aob(std::string_view name, uintptr_t module_base, size_t module_size, | ||
| 613 | std::string_view aob_pattern_str, ptrdiff_t aob_offset, void *detour_function, | ||
| 614 | void **original_trampoline, const HookConfig &config = HookConfig()); | ||
| 615 | |||
| 616 | /** | ||
| 617 | * @brief Creates a mid-function hook at a specific target memory address. | ||
| 618 | * @param name A unique, descriptive name for the hook. | ||
| 619 | * @param target_address The memory address within a function to hook. | ||
| 620 | * @param detour_function The function to be called when the mid-hook is executed. | ||
| 621 | * @param config Optional configuration settings for the hook. | ||
| 622 | * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise. | ||
| 623 | */ | ||
| 624 | [[nodiscard]] std::expected<std::string, HookError> create_mid_hook(std::string_view name, | ||
| 625 | uintptr_t target_address, | ||
| 626 | safetyhook::MidHookFn detour_function, | ||
| 627 | const HookConfig &config = HookConfig()); | ||
| 628 | |||
| 629 | /** | ||
| 630 | * @brief Creates a mid-function hook by finding target address via AOB scan. | ||
| 631 | * @param name A unique, descriptive name for the hook. | ||
| 632 | * @param module_base Base address of the memory module to scan. | ||
| 633 | * @param module_size Size of the memory module to scan. | ||
| 634 | * @param aob_pattern_str The AOB pattern string. | ||
| 635 | * @param aob_offset Offset to add to the found pattern's address. | ||
| 636 | * @param detour_function The mid-hook detour function. | ||
| 637 | * @param config Optional configuration settings for the hook. | ||
| 638 | * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise. | ||
| 639 | * @note The AOB scan over [module_base, module_base + module_size) is page-filtered: it walks VirtualQuery | ||
| 640 | * and skips guard, no-access, and non-readable pages, so passing a full SizeOfImage span is safe even | ||
| 641 | * when the image contains a guard or no-access section -- unlike the raw Scanner::find_pattern | ||
| 642 | * overloads, which read the span unconditionally and fault the host on an unreadable byte. A signature | ||
| 643 | * straddling a protection split inside the image is still found, and @p aob_offset is applied to the | ||
| 644 | * located address. For code that lives outside any mapped module (packed payloads unpacked into | ||
| 645 | * anonymous pages), resolve the address with the whole-process Scanner sweeps and call | ||
| 646 | * create_mid_hook with the result instead. | ||
| 647 | */ | ||
| 648 | [[nodiscard]] std::expected<std::string, HookError> | ||
| 649 | create_mid_hook_aob(std::string_view name, uintptr_t module_base, size_t module_size, | ||
| 650 | std::string_view aob_pattern_str, ptrdiff_t aob_offset, | ||
| 651 | safetyhook::MidHookFn detour_function, const HookConfig &config = HookConfig()); | ||
| 652 | |||
| 653 | /** | ||
| 654 | * @brief Creates a VMT hook for the given object, cloning its vtable. | ||
| 655 | * @param name A unique, descriptive name for the VMT hook. | ||
| 656 | * @param object Pointer to the polymorphic object whose vptr will be replaced. | ||
| 657 | * @return std::expected<std::string, HookError> The hook name if successful, error code otherwise. | ||
| 658 | * @note Setup/control-plane only: clones a vtable, allocates, and takes the HookManager exclusive lock. Call | ||
| 659 | * from init/shutdown or a worker thread, never from a hook or input callback. | ||
| 660 | * @warning VMT hooks have no enable/disable: creation swaps the object's vptr to the cloned table and removal | ||
| 661 | * restores it. Removal is a bare vptr write with no thread protection at all -- weaker than inline/mid | ||
| 662 | * teardown, which at least relocates a thread that faults on the patched page via SafetyHook's | ||
| 663 | * vectored exception handler. A thread already dispatching through the cloned slot can call into the | ||
| 664 | * freed clone, so the caller must guarantee no thread is calling a hooked method on @p object across | ||
| 665 | * create/remove, and that @p object outlives the hook. The vptr must also stay stable for the hook's | ||
| 666 | * lifetime; if the game reconstructs the object in place (rewriting its vptr) the hook is silently | ||
| 667 | * lost. | ||
| 668 | */ | ||
| 669 | [[nodiscard]] std::expected<std::string, HookError> create_vmt_hook(std::string_view name, void *object); | ||
| 670 | |||
| 671 | /** | ||
| 672 | * @brief Configurable VMT hook creation, symmetric with the inline hook's @ref create_inline_hook overload. | ||
| 673 | * @details Single source of truth for VMT hook policy. The single-argument overload above is a thin delegating | ||
| 674 | * wrapper around this one with a default-constructed @ref VmtHookConfig, so call sites that only need | ||
| 675 | * a name and an object compile and behave exactly as before. | ||
| 676 | * @param name A unique, descriptive name for the VMT hook. | ||
| 677 | * @param object Pointer to the polymorphic object whose vptr will be replaced. | ||
| 678 | * @param cfg VMT policy (fail-if-already-hooked, pre-flight slot decoding). | ||
| 679 | * @return std::expected<std::string, HookError> The hook name if successful. @ref HookError::HookAlreadyExists | ||
| 680 | * is returned when @p cfg.fail_if_already_hooked is set and the object's vptr already points at a | ||
| 681 | * vtable cloned by this HookManager. @ref HookError::InvalidObject is returned when | ||
| 682 | * @p cfg.fail_on_non_function_pointer is set and the pre-flight decoder rejects the first byte of | ||
| 683 | * the vtable, and also when either flag is set and the object's vptr or vtable is unreadable. | ||
| 684 | */ | ||
| 685 | [[nodiscard]] std::expected<std::string, HookError> create_vmt_hook(std::string_view name, void *object, | ||
| 686 | const VmtHookConfig &cfg); | ||
| 687 | |||
| 688 | /** | ||
| 689 | * @brief Hooks a specific virtual method by index in a named VMT hook. | ||
| 690 | * @tparam T The type of the destination function (function pointer or member function pointer). | ||
| 691 | * @param vmt_name The name of the VMT hook (from create_vmt_hook). | ||
| 692 | * @param method_index The zero-based vtable index of the method to hook. | ||
| 693 | * @param destination The replacement function. | ||
| 694 | * @return std::expected<size_t, HookError> The method index if successful, error code otherwise. | ||
| 695 | */ | ||
| 696 | template <typename T> | ||
| 697 | 11 | [[nodiscard]] std::expected<size_t, HookError> hook_vmt_method(std::string_view vmt_name, size_t method_index, | |
| 698 | T destination) | ||
| 699 | { | ||
| 700 |
2/4std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 10 times.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
|
11 | if (m_shutdown_called.load(std::memory_order_acquire)) |
| 701 | { | ||
| 702 | ✗ | m_logger.error("HookManager: Shutdown in progress. Cannot hook VMT method on '{}'.", vmt_name); | |
| 703 | ✗ | return std::unexpected(HookError::ShutdownInProgress); | |
| 704 | } | ||
| 705 |
2/4std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 14 taken 10 times.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 14 not taken.
|
11 | if (get_reentrancy_guard() > 0) |
| 706 | { | ||
| 707 |
1/4std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 42 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✓ Branch 10 → 11 taken 1 time.
✗ Branch 10 → 42 not taken.
|
1 | m_logger.error("HookManager: Reentrant hook_vmt_method('{}'/{}) from within a with_*/try_with_* " |
| 708 | "callback rejected; defer hook mutation until the callback returns.", | ||
| 709 | vmt_name, method_index); | ||
| 710 | 1 | return std::unexpected(HookError::ReentrantCallRejected); | |
| 711 | } | ||
| 712 | |||
| 713 | 10 | auto [result, | |
| 714 |
6/136std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✓ Branch 14 → 15 taken 10 times.
✗ Branch 14 → 43 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 43 not taken.
DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int))::{lambda()#1}::operator()() const:
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 110 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 130 not taken.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 37 taken 2 times.
✓ Branch 42 → 43 taken 1 time.
✗ Branch 42 → 150 not taken.
✗ Branch 52 → 53 not taken.
✓ Branch 52 → 54 taken 1 time.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 170 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 91 → 92 not taken.
✓ Branch 91 → 93 taken 7 times.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 115 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 113 → 115 not taken.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 135 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 155 not taken.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 175 not taken.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
✗ Branch 190 → 191 not taken.
✗ Branch 190 → 192 not taken.
✗ Branch 194 → 195 not taken.
✗ Branch 194 → 198 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 198 not taken.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 269 not taken.
✗ Branch 233 → 234 not taken.
✗ Branch 233 → 235 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 245 not taken.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 252 not taken.
✗ Branch 250 → 251 not taken.
✗ Branch 250 → 252 not taken.
✗ Branch 266 → 267 not taken.
✗ Branch 266 → 268 not taken.
✗ Branch 270 → 271 not taken.
✗ Branch 270 → 274 not taken.
✗ Branch 272 → 273 not taken.
✗ Branch 272 → 274 not taken.
DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int))::{lambda()#1}::operator()() const:
✗ Branch 7 → 8 not taken.
✗ Branch 7 → 110 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 130 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 150 not taken.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 170 not taken.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
✗ Branch 107 → 108 not taken.
✗ Branch 107 → 109 not taken.
✗ Branch 111 → 112 not taken.
✗ Branch 111 → 115 not taken.
✗ Branch 113 → 114 not taken.
✗ Branch 113 → 115 not taken.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
✗ Branch 131 → 132 not taken.
✗ Branch 131 → 135 not taken.
✗ Branch 133 → 134 not taken.
✗ Branch 133 → 135 not taken.
✗ Branch 147 → 148 not taken.
✗ Branch 147 → 149 not taken.
✗ Branch 151 → 152 not taken.
✗ Branch 151 → 155 not taken.
✗ Branch 153 → 154 not taken.
✗ Branch 153 → 155 not taken.
✗ Branch 167 → 168 not taken.
✗ Branch 167 → 169 not taken.
✗ Branch 171 → 172 not taken.
✗ Branch 171 → 175 not taken.
✗ Branch 173 → 174 not taken.
✗ Branch 173 → 175 not taken.
✗ Branch 190 → 191 not taken.
✗ Branch 190 → 192 not taken.
✗ Branch 194 → 195 not taken.
✗ Branch 194 → 198 not taken.
✗ Branch 196 → 197 not taken.
✗ Branch 196 → 198 not taken.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
✗ Branch 223 → 224 not taken.
✗ Branch 223 → 269 not taken.
✗ Branch 233 → 234 not taken.
✗ Branch 233 → 235 not taken.
✗ Branch 243 → 244 not taken.
✗ Branch 243 → 245 not taken.
✗ Branch 248 → 249 not taken.
✗ Branch 248 → 252 not taken.
✗ Branch 250 → 251 not taken.
✗ Branch 250 → 252 not taken.
✗ Branch 266 → 267 not taken.
✗ Branch 266 → 268 not taken.
✗ Branch 270 → 271 not taken.
✗ Branch 270 → 274 not taken.
✗ Branch 272 → 273 not taken.
✗ Branch 272 → 274 not taken.
|
33 | deferred_logs] = [&]() -> std::pair<std::expected<size_t, HookError>, std::vector<DeferredLogEntry>> |
| 715 | { | ||
| 716 | 10 | std::shared_lock<detail::SrwSharedMutex> mutator_gate(m_mutator_gate); | |
| 717 |
1/2✓ Branch 3 → 4 taken 10 times.
✗ Branch 3 → 283 not taken.
|
10 | std::unique_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); |
| 718 | |||
| 719 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 20 taken 10 times.
|
10 | if (m_shutdown_called.load(std::memory_order_acquire)) |
| 720 | { | ||
| 721 | return { | ||
| 722 | ✗ | std::unexpected(HookError::ShutdownInProgress), | |
| 723 | {{std::format("HookManager: Shutdown in progress. Cannot hook VMT method on '{}'.", vmt_name), | ||
| 724 | ✗ | LogLevel::Error}}}; | |
| 725 | } | ||
| 726 | |||
| 727 |
1/2✓ Branch 20 → 21 taken 10 times.
✗ Branch 20 → 281 not taken.
|
10 | auto vmt_it = m_vmt_hooks.find(vmt_name); |
| 728 |
2/2✓ Branch 23 → 24 taken 2 times.
✓ Branch 23 → 38 taken 8 times.
|
10 | if (vmt_it == m_vmt_hooks.end()) |
| 729 | { | ||
| 730 | 2 | return {std::unexpected(HookError::VmtHookNotFound), | |
| 731 | {{std::format("HookManager: VMT hook '{}' not found for method hook at index {}.", vmt_name, | ||
| 732 | method_index), | ||
| 733 |
3/6✓ Branch 28 → 29 taken 2 times.
✗ Branch 28 → 120 not taken.
✓ Branch 33 → 34 taken 2 times.
✓ Branch 33 → 35 taken 2 times.
✗ Branch 124 → 125 not taken.
✗ Branch 124 → 126 not taken.
|
10 | LogLevel::Error}}}; |
| 734 | } | ||
| 735 | |||
| 736 |
2/2✓ Branch 40 → 41 taken 1 time.
✓ Branch 40 → 55 taken 7 times.
|
8 | if (vmt_it->second.has_method_hook(method_index)) |
| 737 | { | ||
| 738 | 1 | return {std::unexpected(HookError::MethodAlreadyHooked), | |
| 739 | {{std::format("HookManager: VMT '{}' method index {} is already hooked.", vmt_name, | ||
| 740 | method_index), | ||
| 741 |
3/6✓ Branch 45 → 46 taken 1 time.
✗ Branch 45 → 140 not taken.
✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 52 taken 1 time.
✗ Branch 144 → 145 not taken.
✗ Branch 144 → 146 not taken.
|
5 | LogLevel::Error}}}; |
| 742 | } | ||
| 743 | |||
| 744 | try | ||
| 745 | { | ||
| 746 |
1/2✓ Branch 57 → 58 taken 7 times.
✗ Branch 57 → 203 not taken.
|
7 | auto hook_result = vmt_it->second.vmt_hook().hook_method(method_index, destination); |
| 747 | |||
| 748 |
1/2✗ Branch 59 → 60 not taken.
✓ Branch 59 → 74 taken 7 times.
|
7 | if (!hook_result) |
| 749 | { | ||
| 750 | ✗ | return {std::unexpected(HookError::SafetyHookError), | |
| 751 | {{std::format("HookManager: Failed to hook VMT '{}' method index {}.", vmt_name, | ||
| 752 | method_index), | ||
| 753 | ✗ | LogLevel::Error}}}; | |
| 754 | } | ||
| 755 | |||
| 756 |
2/4✓ Branch 75 → 76 taken 7 times.
✗ Branch 75 → 182 not taken.
✓ Branch 79 → 80 taken 7 times.
✗ Branch 79 → 180 not taken.
|
14 | vmt_it->second.add_method_hook(method_index, std::move(hook_result.value())); |
| 757 | |||
| 758 | return {method_index, | ||
| 759 | {{std::format("HookManager: Successfully hooked VMT '{}' method index {}.", vmt_name, | ||
| 760 | method_index), | ||
| 761 |
4/8✓ Branch 81 → 82 taken 7 times.
✗ Branch 81 → 193 not taken.
✓ Branch 84 → 85 taken 7 times.
✗ Branch 84 → 183 not taken.
✓ Branch 89 → 90 taken 7 times.
✓ Branch 89 → 91 taken 7 times.
✗ Branch 187 → 188 not taken.
✗ Branch 187 → 189 not taken.
|
35 | LogLevel::Info}}}; |
| 762 | 7 | } | |
| 763 | ✗ | catch (const std::exception &e) | |
| 764 | { | ||
| 765 | ✗ | return {std::unexpected(HookError::UnknownError), | |
| 766 | {{std::format("HookManager: Exception hooking VMT '{}' method index {}: {}", vmt_name, | ||
| 767 | ✗ | method_index, e.what()), | |
| 768 | ✗ | LogLevel::Error}}}; | |
| 769 | } | ||
| 770 | ✗ | catch (...) | |
| 771 | { | ||
| 772 | ✗ | return {std::unexpected(HookError::UnknownError), | |
| 773 | {{std::format("HookManager: Unknown exception hooking VMT '{}' method index {}.", vmt_name, | ||
| 774 | method_index), | ||
| 775 | ✗ | LogLevel::Error}}}; | |
| 776 | } | ||
| 777 | 10 | }(); | |
| 778 | |||
| 779 |
2/4std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✓ Branch 32 → 19 taken 10 times.
✓ Branch 32 → 33 taken 10 times.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 32 → 19 not taken.
✗ Branch 32 → 33 not taken.
|
30 | for (const auto &entry : deferred_logs) |
| 780 | { | ||
| 781 |
1/4std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✓ Branch 22 → 23 taken 10 times.
✗ Branch 22 → 44 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 44 not taken.
|
10 | m_logger.log(entry.level, entry.msg); |
| 782 | } | ||
| 783 | 10 | return result; | |
| 784 |
1/8std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (VmtTestHook::*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (VmtTestHook::*)(int, int)):
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 10 times.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
std::expected<unsigned long long, DetourModKit::HookError> DetourModKit::HookManager::hook_vmt_method<int (*)(int, int)>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, int (*)(int, int)):
✗ Branch 33 → 34 not taken.
✗ Branch 33 → 35 not taken.
✗ Branch 45 → 46 not taken.
✗ Branch 45 → 47 not taken.
|
20 | } |
| 785 | |||
| 786 | /** | ||
| 787 | * @brief Removes an entire VMT hook, restoring the original vtable on all applied objects. | ||
| 788 | * @details Bulk teardown (remove_all_vmt_hooks, remove_all_hooks, shutdown, destructor) destroys VMT hooks | ||
| 789 | * in reverse creation order so clones layered on the same object unwind safely. Explicit removal | ||
| 790 | * does not reorder for the caller: removing an inner layer while a clone created later on top of it | ||
| 791 | * is still installed frees the clone that the newer hook recorded as its "original", so remove | ||
| 792 | * layered hooks newest-first. | ||
| 793 | * @warning Restoring a VMT hook writes the saved original vptr back into the object. If the game reconstructed | ||
| 794 | * the object in place or layered its own vptr on top after the clone was installed, this write | ||
| 795 | * restores a vtable the game has since overwritten, silently clobbering the game's pointer (or | ||
| 796 | * restoring a stale one). VMT removal is a bare vptr write with no thread protection, so the caller | ||
| 797 | * must guarantee no thread is dispatching through the cloned slot across removal and that the object's | ||
| 798 | * vptr was not re-layered since create. | ||
| 799 | * @note Two-phase teardown / quiesce contract: the registry entry is mutated under the exclusive lock; do not | ||
| 800 | * call this from within a with_* / try_with_* callback (defer until the callback returns -- the | ||
| 801 | * reentrancy guard fails such calls closed). | ||
| 802 | * @param vmt_name The name of the VMT hook to remove. | ||
| 803 | * @return Success if removed, or HookError::VmtHookNotFound. | ||
| 804 | */ | ||
| 805 | [[nodiscard]] std::expected<void, HookError> remove_vmt_hook(std::string_view vmt_name); | ||
| 806 | |||
| 807 | /** | ||
| 808 | * @brief Removes a single method hook from a VMT, restoring the original method. | ||
| 809 | * @warning Restoring a method hook rewrites the cloned vtable slot back to the original function pointer. If | ||
| 810 | * the game overwrote that slot or relaid the object's vptr after the hook was installed, the restore | ||
| 811 | * writes over a pointer the game has since changed. Removal carries no thread protection, so the | ||
| 812 | * caller must guarantee no thread is dispatching through the slot across removal. | ||
| 813 | * @note Two-phase teardown / quiesce contract: mutates the registry entry under the exclusive lock; do not call | ||
| 814 | * this from within a with_* / try_with_* callback (defer until the callback returns -- the reentrancy | ||
| 815 | * guard fails such calls closed). | ||
| 816 | * @param vmt_name The name of the VMT hook. | ||
| 817 | * @param method_index The vtable index of the method to unhook. | ||
| 818 | * @return Success if removed, or a HookError describing the failure. | ||
| 819 | */ | ||
| 820 | [[nodiscard]] std::expected<void, HookError> remove_vmt_method(std::string_view vmt_name, size_t method_index); | ||
| 821 | |||
| 822 | /** | ||
| 823 | * @brief Applies the cloned (hooked) vtable to an additional object. | ||
| 824 | * @param vmt_name The name of the VMT hook. | ||
| 825 | * @param object The object to apply the hooked vtable to. | ||
| 826 | * @return true if the VMT hook was found and applied, false otherwise. | ||
| 827 | */ | ||
| 828 | [[nodiscard]] bool apply_vmt_hook(std::string_view vmt_name, void *object); | ||
| 829 | |||
| 830 | /** | ||
| 831 | * @brief Configurable form of @ref apply_vmt_hook, symmetric with @ref create_vmt_hook. | ||
| 832 | * @details The two-argument overload above is a thin delegating wrapper that uses a default-constructed | ||
| 833 | * @ref VmtHookConfig, preserving the historical permissive apply semantics (apply still returns | ||
| 834 | * false on shutdown, a null object, an unknown name, or an apply exception). | ||
| 835 | * @param vmt_name The name of the VMT hook whose cloned vtable should be installed. | ||
| 836 | * @param object The object to apply the cloned vtable to. | ||
| 837 | * @param cfg Apply policy. @p cfg.fail_if_already_hooked lets a mod refuse to install its vtable on an | ||
| 838 | * object that is already on a clone from this HookManager (a re-apply of the same clone is a | ||
| 839 | * no-op for SafetyHook; the guard exists for symmetry and for callers that want a single | ||
| 840 | * create/apply that is a no-op on a re-invocation). @p cfg.fail_on_non_function_pointer re-runs | ||
| 841 | * the pre-flight decoder against the vtable currently installed on the object (the one about to | ||
| 842 | * be replaced). | ||
| 843 | * @return true if the VMT hook was found and applied, false otherwise. | ||
| 844 | */ | ||
| 845 | [[nodiscard]] bool apply_vmt_hook(std::string_view vmt_name, void *object, const VmtHookConfig &cfg); | ||
| 846 | |||
| 847 | /** | ||
| 848 | * @brief Removes the hooked vtable from a specific object, restoring its original vptr. | ||
| 849 | * @warning This writes the saved original vptr back into @p object. If the game reconstructed @p object in | ||
| 850 | * place | ||
| 851 | * or layered its own vptr on top after the clone was installed, the restore overwrites the vptr the | ||
| 852 | * game has since set, silently clobbering it (or installing a stale vtable). The write has no thread | ||
| 853 | * protection, so the caller must guarantee no thread is dispatching through the cloned slot across the | ||
| 854 | * restore and that the object's vptr was not re-layered since the clone went on. | ||
| 855 | * @note Two-phase teardown / quiesce contract: mutates the registry entry under the exclusive lock; do not call | ||
| 856 | * this from within a with_* / try_with_* callback (defer until the callback returns -- the reentrancy | ||
| 857 | * guard fails such calls closed). | ||
| 858 | * @param vmt_name The name of the VMT hook. | ||
| 859 | * @param object The object to restore. | ||
| 860 | * @return true if the VMT hook was found and the object was restored, false otherwise. | ||
| 861 | */ | ||
| 862 | [[nodiscard]] bool remove_vmt_from_object(std::string_view vmt_name, void *object); | ||
| 863 | |||
| 864 | /** | ||
| 865 | * @brief Removes all VMT hooks, restoring original vtables on all applied objects. | ||
| 866 | * @details Destroys VMT hooks newest-first so clones layered on the same object unwind safely. | ||
| 867 | * @warning Each restore writes a saved original vptr back into its applied objects. If the game reconstructed | ||
| 868 | * an | ||
| 869 | * object in place or layered its own vptr on top after the clone was installed, the restore overwrites | ||
| 870 | * the vptr the game has since set, silently clobbering it (or installing a stale vtable). VMT removal | ||
| 871 | * is a bare vptr write with no thread protection, so the caller must quiesce all hooked objects -- no | ||
| 872 | * thread dispatching through any cloned slot -- across this teardown. | ||
| 873 | * @note Two-phase teardown / quiesce contract: mutates the registry under the exclusive lock; do not call this | ||
| 874 | * (or any mutator) from within a with_* / try_with_* callback (defer until the callback returns -- the | ||
| 875 | * reentrancy guard fails such calls closed). | ||
| 876 | */ | ||
| 877 | void remove_all_vmt_hooks(); | ||
| 878 | |||
| 879 | /** | ||
| 880 | * @brief Returns the names of all active VMT hooks. | ||
| 881 | * @return std::vector<std::string> Vector containing the names of the VMT hooks. | ||
| 882 | */ | ||
| 883 | [[nodiscard]] std::vector<std::string> get_vmt_hook_names() const; | ||
| 884 | |||
| 885 | /** | ||
| 886 | * @brief Safely accesses a VmHook (method hook) within a named VMT hook. | ||
| 887 | * @details The callback is invoked while the hook registry is held under a reader lock. | ||
| 888 | * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor | ||
| 889 | * from the callback (each checks the reentrancy guard and fails closed). Queue mutations and apply | ||
| 890 | * them after the callback returns. | ||
| 891 | * @tparam F Callable type accepting (safetyhook::VmHook&) and returning a value. | ||
| 892 | * @param vmt_name The name of the VMT hook. | ||
| 893 | * @param method_index The vtable index of the method hook. | ||
| 894 | * @param fn The callback to invoke with the VmHook reference. | ||
| 895 | * @return std::optional<R> The callback's return value, or std::nullopt if not found. | ||
| 896 | */ | ||
| 897 | template <typename F> | ||
| 898 | requires std::invocable<F, safetyhook::VmHook &> && | ||
| 899 | (!std::is_void_v<std::invoke_result_t<F, safetyhook::VmHook &>>) && | ||
| 900 | (!std::is_reference_v<std::invoke_result_t<F, safetyhook::VmHook &>>) | ||
| 901 | 3 | [[nodiscard]] auto with_vmt_method(std::string_view vmt_name, size_t method_index, F &&fn) | |
| 902 | -> std::optional<std::invoke_result_t<F, safetyhook::VmHook &>> | ||
| 903 | { | ||
| 904 |
3/6std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
|
3 | if (get_reentrancy_guard() > 0) |
| 905 | { | ||
| 906 | ✗ | m_logger.error("HookManager: Reentrant callback detected in with_vmt_method('{}'/{})!", vmt_name, | |
| 907 | method_index); | ||
| 908 | ✗ | return std::nullopt; | |
| 909 | } | ||
| 910 | 3 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); | |
| 911 | 3 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 912 |
3/6std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 34 not taken.
|
3 | auto vmt_it = m_vmt_hooks.find(vmt_name); |
| 913 |
3/6std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 24 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 24 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 24 not taken.
|
3 | if (vmt_it != m_vmt_hooks.end()) |
| 914 | { | ||
| 915 |
2/6std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 34 not taken.
|
2 | auto *vm_hook = vmt_it->second.get_method_hook(method_index); |
| 916 |
2/6std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 24 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 24 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 24 taken 1 time.
|
2 | if (vm_hook) |
| 917 | { | ||
| 918 |
1/6std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_NotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 32 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_ValueCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 20 → 21 taken 1 time.
✗ Branch 20 → 32 not taken.
std::optional<std::invoke_result<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}, safetyhook::VmHook&>::type> DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_MethodNotFound_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 32 not taken.
|
1 | return std::invoke(std::forward<F>(fn), *vm_hook); |
| 919 | } | ||
| 920 | } | ||
| 921 | 2 | return std::nullopt; | |
| 922 | 3 | } | |
| 923 | |||
| 924 | /** | ||
| 925 | * @brief Safely accesses a VmHook for a void-returning callback. | ||
| 926 | * @details Same locking and reentrancy semantics as the value-returning overload. | ||
| 927 | * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor | ||
| 928 | * from the callback (each checks the reentrancy guard and fails closed). Queue mutations and apply | ||
| 929 | * them after the callback returns. | ||
| 930 | * @param vmt_name The name of the VMT hook. | ||
| 931 | * @param method_index The vtable index of the method hook. | ||
| 932 | * @param fn The void-returning callback to invoke with the VmHook reference. | ||
| 933 | * @return true if the method hook was found and the callback was invoked, false otherwise. | ||
| 934 | */ | ||
| 935 | template <typename F> | ||
| 936 | requires std::invocable<F, safetyhook::VmHook &> && | ||
| 937 | std::is_void_v<std::invoke_result_t<F, safetyhook::VmHook &>> | ||
| 938 | 5 | [[nodiscard]] bool with_vmt_method(std::string_view vmt_name, size_t method_index, F &&fn) | |
| 939 | { | ||
| 940 |
5/10bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
|
5 | if (get_reentrancy_guard() > 0) |
| 941 | { | ||
| 942 | ✗ | m_logger.error("HookManager: Reentrant callback detected in with_vmt_method('{}'/{})!", vmt_name, | |
| 943 | method_index); | ||
| 944 | ✗ | return false; | |
| 945 | } | ||
| 946 | 5 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); | |
| 947 | 5 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 948 |
5/10bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 26 not taken.
|
5 | auto vmt_it = m_vmt_hooks.find(vmt_name); |
| 949 |
5/10bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 20 not taken.
|
5 | if (vmt_it != m_vmt_hooks.end()) |
| 950 | { | ||
| 951 |
5/10bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 26 not taken.
|
5 | auto *vm_hook = vmt_it->second.get_method_hook(method_index); |
| 952 |
5/10bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 20 not taken.
|
5 | if (vm_hook) |
| 953 | { | ||
| 954 |
5/10bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_HookMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveMethod_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_RemoveEntireHook_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_ApplyToMultipleObjects_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
bool DetourModKit::HookManager::with_vmt_method<HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, unsigned long long, HookManagerTest_VmtHook_WithVmtMethod_VoidCallback_Test::TestBody()::{lambda(safetyhook::VmHook&)#1}&&):
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 26 not taken.
|
5 | std::invoke(std::forward<F>(fn), *vm_hook); |
| 955 | 5 | return true; | |
| 956 | } | ||
| 957 | } | ||
| 958 | ✗ | return false; | |
| 959 | 5 | } | |
| 960 | |||
| 961 | /** | ||
| 962 | * @brief Reports whether this manager patches @p target_address. | ||
| 963 | * @details Inline and mid hooks both patch the target prologue, so both are reported here. The query walks only | ||
| 964 | * this HookManager's registry; hooks installed by other statically-linked DMK consumers in the same | ||
| 965 | * process are not visible. | ||
| 966 | * | ||
| 967 | * Use this to short-circuit redundant create_inline_hook or create_mid_hook calls. To detect hooks | ||
| 968 | * installed outside this manager, pass HookConfig::fail_if_already_hooked during creation. | ||
| 969 | * @param target_address Function address to query. | ||
| 970 | * @return true if a managed inline or mid hook already targets this address. | ||
| 971 | */ | ||
| 972 | [[nodiscard]] bool is_target_already_hooked(uintptr_t target_address) const noexcept; | ||
| 973 | |||
| 974 | /** | ||
| 975 | * @brief Removes a hook identified by its name. | ||
| 976 | * @details Bulk teardown (remove_all_hooks, shutdown, destructor) disables and destroys hooks in reverse | ||
| 977 | * creation order so hooks layered on one target address unwind safely. Explicit single removal does | ||
| 978 | * not reorder for the caller: removing an older hook while a newer hook layered on the same address is | ||
| 979 | * still installed restores the prologue to the original bytes underneath the newer hook, leaving its | ||
| 980 | * entry jump pointing into a trampoline that is about to be freed. Remove layered hooks newest-first. | ||
| 981 | * @note Two-phase teardown / quiesce contract: disables the hook under the shared registry lock, then erases | ||
| 982 | * its | ||
| 983 | * entry under the exclusive lock. SafetyHook relocates a thread caught in the patched prologue but cannot | ||
| 984 | * drain a thread already inside the detour or trampoline body, so the caller must quiesce the hooked | ||
| 985 | * function before removal to close that residual window. Do not call this from within a with_* / | ||
| 986 | * try_with_* callback; defer the removal until the callback returns (the reentrancy guard fails such | ||
| 987 | * calls closed). | ||
| 988 | * @param hook_id The name of the hook to remove. | ||
| 989 | * @return Success if removed, or HookError::HookNotFound. | ||
| 990 | */ | ||
| 991 | [[nodiscard]] std::expected<void, HookError> remove_hook(std::string_view hook_id); | ||
| 992 | |||
| 993 | /** | ||
| 994 | * @brief Removes all hooks currently managed by this HookManager instance. | ||
| 995 | * @details Uses two-phase removal: disables all hooks under a shared lock first, then clears the maps under an | ||
| 996 | * exclusive lock. Both phases walk the hooks in reverse creation order so hooks layered on one target | ||
| 997 | * address unwind newest-first and each prologue restore writes onto still-valid bytes rather than into | ||
| 998 | * a freed trampoline. The shared phase lets DetourModKit's own with_* readers finish before Hook | ||
| 999 | * storage is destroyed. SafetyHook can relocate threads caught in the patched prologue, but it cannot | ||
| 1000 | * drain threads already running the detour or trampoline body; callers must quiesce the hooked | ||
| 1001 | * function during planned teardown to close that residual window. Do not call this (or any mutator) | ||
| 1002 | * from within a with_* / try_with_* callback; defer the teardown until the callback returns (the | ||
| 1003 | * reentrancy guard fails such calls closed). | ||
| 1004 | * | ||
| 1005 | * After clearing, resets the internal shutdown flag to false, allowing subsequent create_*_hook() | ||
| 1006 | * calls to succeed for hot-reload workflows. | ||
| 1007 | */ | ||
| 1008 | void remove_all_hooks() noexcept; | ||
| 1009 | |||
| 1010 | /** | ||
| 1011 | * @brief Enables a previously disabled hook. | ||
| 1012 | * @details Idempotent: enabling an already-active hook returns success. Returns HookError::InvalidHookState | ||
| 1013 | * only when the hook is in a transitional state (Enabling or Disabling). Other HookError values | ||
| 1014 | * indicate lookup or SafetyHook failures. | ||
| 1015 | * @param hook_id The name of the hook to enable. | ||
| 1016 | * @return Success if the hook is now active (or was already active), or a HookError describing the failure. | ||
| 1017 | */ | ||
| 1018 | [[nodiscard]] std::expected<void, HookError> enable_hook(std::string_view hook_id); | ||
| 1019 | |||
| 1020 | /** | ||
| 1021 | * @brief Disables an active hook temporarily without removing it. | ||
| 1022 | * @details Idempotent: disabling an already-disabled hook returns success. Returns HookError::InvalidHookState | ||
| 1023 | * only when the hook is in a transitional state (Enabling or Disabling). Other HookError values | ||
| 1024 | * indicate lookup or SafetyHook failures. | ||
| 1025 | * @param hook_id The name of the hook to disable. | ||
| 1026 | * @return Success if the hook is now disabled (or was already disabled), or a HookError describing the failure. | ||
| 1027 | */ | ||
| 1028 | [[nodiscard]] std::expected<void, HookError> disable_hook(std::string_view hook_id); | ||
| 1029 | |||
| 1030 | /** | ||
| 1031 | * @brief Enables several hooks by name in a single locked pass. | ||
| 1032 | * @details Convenience for startup and hot-reload phases that toggle many hooks at once: takes the manager's | ||
| 1033 | * locks once for the whole batch instead of once per hook. An unknown id is warned and skipped, not | ||
| 1034 | * fatal, and an already-active hook counts as a success (enable is idempotent). This is an ergonomic | ||
| 1035 | * wrapper, not a performance optimization over repeated enable_hook calls: the SafetyHook backend | ||
| 1036 | * installs via a vectored exception handler and does not suspend threads, so there is no process-wide | ||
| 1037 | * suspension to amortize. | ||
| 1038 | * @param hook_ids The names of the hooks to enable. | ||
| 1039 | * @return The number of hooks now active. | ||
| 1040 | */ | ||
| 1041 | [[nodiscard]] std::size_t enable_hooks(std::span<const std::string_view> hook_ids); | ||
| 1042 | |||
| 1043 | /** | ||
| 1044 | * @brief Disables several hooks by name in a single locked pass. | ||
| 1045 | * @details The disable counterpart to @ref enable_hooks: locks once, warns and skips unknown ids, and counts an | ||
| 1046 | * already-disabled hook as a success (disable is idempotent). Ergonomics only (see @ref enable_hooks). | ||
| 1047 | * @param hook_ids The names of the hooks to disable. | ||
| 1048 | * @return The number of hooks now disabled. | ||
| 1049 | */ | ||
| 1050 | [[nodiscard]] std::size_t disable_hooks(std::span<const std::string_view> hook_ids); | ||
| 1051 | |||
| 1052 | /** | ||
| 1053 | * @brief Enables every hook currently managed by this instance in one pass. | ||
| 1054 | * @return The number of hooks now active. | ||
| 1055 | */ | ||
| 1056 | [[nodiscard]] std::size_t enable_all_hooks(); | ||
| 1057 | |||
| 1058 | /** | ||
| 1059 | * @brief Disables every hook currently managed by this instance in one pass. | ||
| 1060 | * @return The number of hooks now disabled. | ||
| 1061 | */ | ||
| 1062 | [[nodiscard]] std::size_t disable_all_hooks(); | ||
| 1063 | |||
| 1064 | /** | ||
| 1065 | * @brief Retrieves the current status of a hook. | ||
| 1066 | * @param hook_id The name of the hook. | ||
| 1067 | * @return std::optional<HookStatus> The current status, or std::nullopt if not found. | ||
| 1068 | */ | ||
| 1069 | [[nodiscard]] std::optional<HookStatus> get_hook_status(std::string_view hook_id) const; | ||
| 1070 | |||
| 1071 | /** | ||
| 1072 | * @brief Gets a summary of hook counts categorized by their status. | ||
| 1073 | * @return std::unordered_map<HookStatus, size_t> Map of statuses to counts. | ||
| 1074 | */ | ||
| 1075 | [[nodiscard]] std::unordered_map<HookStatus, size_t> get_hook_counts() const; | ||
| 1076 | |||
| 1077 | /** | ||
| 1078 | * @brief Retrieves a list of hook names. | ||
| 1079 | * @param status_filter Optional status filter for returned hooks. | ||
| 1080 | * @return std::vector<std::string> Vector containing the names of the hooks. | ||
| 1081 | */ | ||
| 1082 | [[nodiscard]] std::vector<std::string> | ||
| 1083 | 56 | get_hook_ids(std::optional<HookStatus> status_filter = std::nullopt) const; | |
| 1084 | |||
| 1085 | // clang-format off | ||
| 1086 | /** | ||
| 1087 | * @brief Safely accesses an InlineHook by its ID while holding the internal lock. | ||
| 1088 | * @details The callback receives an InlineHook reference while the hook registry is held under a reader lock. | ||
| 1089 | * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor | ||
| 1090 | * from the callback. The callback holds m_hooks_mutex shared: create/remove/teardown paths acquire it | ||
| 1091 | * exclusively and toggle paths re-acquire it shared (UB on a non-recursive lock), while nested | ||
| 1092 | * accessors check the reentrancy guard and fail closed. Queue mutations and apply them after the | ||
| 1093 | * callback returns. | ||
| 1094 | * @tparam F Callable type accepting (InlineHook&) and returning a value. | ||
| 1095 | * @param hook_id The name of the inline hook. | ||
| 1096 | * @param fn The callback to invoke with the hook reference. | ||
| 1097 | * @return The callback's return value, or std::nullopt if the hook was not found. | ||
| 1098 | */ | ||
| 1099 | // clang-format on | ||
| 1100 | template <typename F> | ||
| 1101 | requires std::invocable<F, InlineHook &> && (!std::is_void_v<std::invoke_result_t<F, InlineHook &>>) && | ||
| 1102 | (!std::is_reference_v<std::invoke_result_t<F, InlineHook &>>) | ||
| 1103 | 10 | [[nodiscard]] auto with_inline_hook(std::string_view hook_id, F &&fn) | |
| 1104 | -> std::optional<std::invoke_result_t<F, InlineHook &>> | ||
| 1105 | { | ||
| 1106 |
10/20std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
|
10 | if (get_reentrancy_guard() > 0) |
| 1107 | { | ||
| 1108 | ✗ | m_logger.error("HookManager: Reentrant callback detected in with_inline_hook('{}')! " | |
| 1109 | "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. " | ||
| 1110 | "Perform mutations outside the callback or use an asynchronous operation.", | ||
| 1111 | hook_id); | ||
| 1112 | ✗ | return std::nullopt; | |
| 1113 | } | ||
| 1114 | 10 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); | |
| 1115 | 10 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 1116 |
10/20std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 35 not taken.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
|
10 | auto it = m_hooks.find(hook_id); |
| 1117 |
28/60std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 1 time.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 28 not taken.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 1 time.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
|
10 | if (it != m_hooks.end() && it->second->get_type() == HookType::Inline) |
| 1118 | { | ||
| 1119 |
7/20std::optional<std::invoke_result<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_WrongType_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealInlineHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_InlineHook_GetOriginal_Noexcept_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_Reentrancy_MutatorsFromCallbackFailClosed_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookIntegrationTest_QueryAccessorsAreReentrantFromCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_ReturnsNulloptForNonExistentHook_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
|
14 | return std::invoke(std::forward<F>(fn), static_cast<InlineHook &>(*it->second)); |
| 1120 | } | ||
| 1121 | 3 | return std::nullopt; | |
| 1122 | 10 | } | |
| 1123 | |||
| 1124 | /** | ||
| 1125 | * @brief Safely accesses an InlineHook by its ID for a void-returning callback. | ||
| 1126 | * @details Same locking and reentrancy semantics as the value-returning overload. | ||
| 1127 | * @param hook_id The name of the inline hook. | ||
| 1128 | * @param fn The void-returning callback to invoke with the hook reference. | ||
| 1129 | * @return true if the hook was found and the callback was invoked, false otherwise. | ||
| 1130 | */ | ||
| 1131 | template <typename F> | ||
| 1132 | requires std::invocable<F, InlineHook &> && std::is_void_v<std::invoke_result_t<F, InlineHook &>> | ||
| 1133 | 3 | [[nodiscard]] bool with_inline_hook(std::string_view hook_id, F &&fn) | |
| 1134 | { | ||
| 1135 |
3/6bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
|
3 | if (get_reentrancy_guard() > 0) |
| 1136 | { | ||
| 1137 | ✗ | m_logger.error("HookManager: Reentrant callback detected in with_inline_hook('{}')! " | |
| 1138 | "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. " | ||
| 1139 | "Perform mutations outside the callback or use an asynchronous operation.", | ||
| 1140 | hook_id); | ||
| 1141 | ✗ | return false; | |
| 1142 | } | ||
| 1143 | 3 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); | |
| 1144 | 3 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 1145 |
3/6bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
|
3 | auto it = m_hooks.find(hook_id); |
| 1146 |
8/18bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 26 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 1 time.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 26 taken 1 time.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 26 not taken.
|
3 | if (it != m_hooks.end() && it->second->get_type() == HookType::Inline) |
| 1147 | { | ||
| 1148 |
2/6bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithInlineHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 32 not taken.
bool DetourModKit::HookManager::with_inline_hook<HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_LateShutdown_DrainsReadersBeforeClearingMaps_Test::TestBody()::{lambda()#1}::operator()() const::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
|
4 | std::invoke(std::forward<F>(fn), static_cast<InlineHook &>(*it->second)); |
| 1149 | 2 | return true; | |
| 1150 | } | ||
| 1151 | 1 | return false; | |
| 1152 | 3 | } | |
| 1153 | |||
| 1154 | /** | ||
| 1155 | * @brief Try-safe access to an InlineHook by its ID using a non-blocking lock. | ||
| 1156 | * @details Provides a non-blocking alternative to with_inline_hook(). The callback is invoked only if the lock | ||
| 1157 | * is immediately acquired via std::try_to_lock. Note: try_to_lock only avoids blocking on initial | ||
| 1158 | * acquisition - it does NOT make callbacks safe to re-enter HookManager mutators or teardown methods | ||
| 1159 | * that also acquire the same non-recursive mutex. If a callback needs to call those methods, it must | ||
| 1160 | * release the lock first or perform those calls asynchronously to avoid deadlock. See with_inline_hook | ||
| 1161 | * for the blocking analogue. | ||
| 1162 | * @param hook_id The name of the inline hook. | ||
| 1163 | * @param fn The callback to invoke with the hook reference. | ||
| 1164 | * @return std::optional<R> The callback's return value. Returns std::nullopt if either the lock could not be | ||
| 1165 | * acquired or the hook was not found. | ||
| 1166 | */ | ||
| 1167 | template <typename F> | ||
| 1168 | requires std::invocable<F, InlineHook &> && (!std::is_void_v<std::invoke_result_t<F, InlineHook &>>) && | ||
| 1169 | (!std::is_reference_v<std::invoke_result_t<F, InlineHook &>>) | ||
| 1170 | 3 | [[nodiscard]] auto try_with_inline_hook(std::string_view hook_id, F &&fn) | |
| 1171 | -> std::optional<std::invoke_result_t<F, InlineHook &>> | ||
| 1172 | { | ||
| 1173 |
3/6std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
|
3 | if (get_reentrancy_guard() > 0) |
| 1174 | { | ||
| 1175 | ✗ | m_logger.error("HookManager: Reentrant callback detected in try_with_inline_hook('{}')! " | |
| 1176 | "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. " | ||
| 1177 | "Perform mutations outside the callback or use an asynchronous operation.", | ||
| 1178 | hook_id); | ||
| 1179 | ✗ | return std::nullopt; | |
| 1180 | } | ||
| 1181 | 3 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex, std::try_to_lock); | |
| 1182 |
3/6std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
|
3 | if (!lock.owns_lock()) |
| 1183 | { | ||
| 1184 | ✗ | return std::nullopt; | |
| 1185 | } | ||
| 1186 | 3 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 1187 |
3/6std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
|
3 | auto it = m_hooks.find(hook_id); |
| 1188 |
8/18std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 21 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 31 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 25 taken 1 time.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 35 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 25 not taken.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 35 not taken.
|
3 | if (it != m_hooks.end() && it->second->get_type() == HookType::Inline) |
| 1189 | { | ||
| 1190 |
2/6std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_Success_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 37 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_NotFound_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 43 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}, DetourModKit::InlineHook&>::type> DetourModKit::HookManager::try_with_inline_hook<HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithInlineHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::InlineHook&)#1}&&):
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 43 not taken.
|
4 | return std::invoke(std::forward<F>(fn), static_cast<InlineHook &>(*it->second)); |
| 1191 | } | ||
| 1192 | 1 | return std::nullopt; | |
| 1193 | 3 | } | |
| 1194 | |||
| 1195 | // clang-format off | ||
| 1196 | /** | ||
| 1197 | * @brief Safely accesses a MidHook by its ID while holding the internal lock. | ||
| 1198 | * @details The callback receives a MidHook reference while the hook registry is held under a reader lock. | ||
| 1199 | * @warning Do not call HookManager mutators, teardown entry points, or a nested with_* or try_with_* accessor | ||
| 1200 | * from the callback. The callback holds m_hooks_mutex shared: create/remove/teardown paths acquire it | ||
| 1201 | * exclusively and toggle paths re-acquire it shared (UB on a non-recursive lock), while nested | ||
| 1202 | * accessors check the reentrancy guard and fail closed. Queue mutations and apply them after the | ||
| 1203 | * callback returns. | ||
| 1204 | * @tparam F Callable type accepting (MidHook&) and returning a value. | ||
| 1205 | * @param hook_id The name of the mid hook. | ||
| 1206 | * @param fn The callback to invoke with the hook reference. | ||
| 1207 | * @return The callback's return value, or std::nullopt if the hook was not found. | ||
| 1208 | */ | ||
| 1209 | // clang-format on | ||
| 1210 | template <typename F> | ||
| 1211 | requires std::invocable<F, MidHook &> && (!std::is_void_v<std::invoke_result_t<F, MidHook &>>) && | ||
| 1212 | (!std::is_reference_v<std::invoke_result_t<F, MidHook &>>) | ||
| 1213 | 7 | [[nodiscard]] auto with_mid_hook(std::string_view hook_id, F &&fn) | |
| 1214 | -> std::optional<std::invoke_result_t<F, MidHook &>> | ||
| 1215 | { | ||
| 1216 |
7/14std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
|
7 | if (get_reentrancy_guard() > 0) |
| 1217 | { | ||
| 1218 | ✗ | m_logger.error("HookManager: Reentrant callback detected in with_mid_hook('{}')! " | |
| 1219 | "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. " | ||
| 1220 | "Perform mutations outside the callback or use an asynchronous operation.", | ||
| 1221 | hook_id); | ||
| 1222 | ✗ | return std::nullopt; | |
| 1223 | } | ||
| 1224 | 7 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); | |
| 1225 | 7 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 1226 |
7/14std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 35 not taken.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 11 → 12 taken 1 time.
✗ Branch 11 → 40 not taken.
|
7 | auto it = m_hooks.find(hook_id); |
| 1227 |
20/42std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 20 taken 1 time.
✗ Branch 18 → 19 not taken.
✗ Branch 18 → 20 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 1 time.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 30 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 28 not taken.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 14 → 15 taken 1 time.
✗ Branch 14 → 20 not taken.
✓ Branch 18 → 19 taken 1 time.
✗ Branch 18 → 20 not taken.
✓ Branch 21 → 22 taken 1 time.
✗ Branch 21 → 30 not taken.
|
7 | if (it != m_hooks.end() && it->second->get_type() == HookType::Mid) |
| 1228 | { | ||
| 1229 |
5/14std::optional<std::invoke_result<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_WrongType_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 26 → 27 not taken.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_RealMidHook_WithCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_SuccessCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 34 not taken.
std::optional<std::invoke_result<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_MidHook_GetDestination_Noexcept_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_DirectEnableDisable_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 38 not taken.
|
10 | return std::invoke(std::forward<F>(fn), static_cast<MidHook &>(*it->second)); |
| 1230 | } | ||
| 1231 | 2 | return std::nullopt; | |
| 1232 | 7 | } | |
| 1233 | |||
| 1234 | /** | ||
| 1235 | * @brief Safely accesses a MidHook by its ID for a void-returning callback. | ||
| 1236 | * @details Same locking and reentrancy semantics as the value-returning overload. | ||
| 1237 | * @param hook_id The name of the mid hook. | ||
| 1238 | * @param fn The void-returning callback to invoke with the hook reference. | ||
| 1239 | * @return true if the hook was found and the callback was invoked, false otherwise. | ||
| 1240 | */ | ||
| 1241 | template <typename F> | ||
| 1242 | requires std::invocable<F, MidHook &> && std::is_void_v<std::invoke_result_t<F, MidHook &>> | ||
| 1243 | 2 | [[nodiscard]] bool with_mid_hook(std::string_view hook_id, F &&fn) | |
| 1244 | { | ||
| 1245 |
2/4bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
|
2 | if (get_reentrancy_guard() > 0) |
| 1246 | { | ||
| 1247 | ✗ | m_logger.error("HookManager: Reentrant callback detected in with_mid_hook('{}')! " | |
| 1248 | "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. " | ||
| 1249 | "Perform mutations outside the callback or use an asynchronous operation.", | ||
| 1250 | hook_id); | ||
| 1251 | ✗ | return false; | |
| 1252 | } | ||
| 1253 | 2 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex); | |
| 1254 | 2 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 1255 |
2/4bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 9 → 10 taken 1 time.
✗ Branch 9 → 32 not taken.
|
2 | auto it = m_hooks.find(hook_id); |
| 1256 |
5/12bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 18 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 26 not taken.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 18 taken 1 time.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 26 taken 1 time.
|
2 | if (it != m_hooks.end() && it->second->get_type() == HookType::Mid) |
| 1257 | { | ||
| 1258 |
1/4bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 32 not taken.
bool DetourModKit::HookManager::with_mid_hook<HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_WithMidHook_VoidCallback_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 32 not taken.
|
2 | std::invoke(std::forward<F>(fn), static_cast<MidHook &>(*it->second)); |
| 1259 | 1 | return true; | |
| 1260 | } | ||
| 1261 | 1 | return false; | |
| 1262 | 2 | } | |
| 1263 | |||
| 1264 | /** | ||
| 1265 | * @brief Try-safe access to a MidHook by its ID using a non-blocking lock. | ||
| 1266 | * @details Provides a non-blocking alternative to with_mid_hook(). The callback is invoked only if the lock is | ||
| 1267 | * immediately acquired via std::try_to_lock. Note: try_to_lock only avoids blocking on initial | ||
| 1268 | * acquisition - it does NOT make callbacks safe to re-enter HookManager mutators or teardown methods | ||
| 1269 | * that also acquire the same non-recursive mutex. If a callback needs to call those methods, it must | ||
| 1270 | * release the lock first or perform those calls asynchronously to avoid deadlock. See with_mid_hook | ||
| 1271 | * for the blocking analogue. | ||
| 1272 | * @param hook_id The name of the mid hook. | ||
| 1273 | * @param fn The callback to invoke with the hook reference. | ||
| 1274 | * @return std::optional<R> The callback's return value. Returns std::nullopt if either the lock could not be | ||
| 1275 | * acquired or the hook was not found. | ||
| 1276 | */ | ||
| 1277 | template <typename F> | ||
| 1278 | requires std::invocable<F, MidHook &> && (!std::is_void_v<std::invoke_result_t<F, MidHook &>>) && | ||
| 1279 | (!std::is_reference_v<std::invoke_result_t<F, MidHook &>>) | ||
| 1280 | 3 | [[nodiscard]] auto try_with_mid_hook(std::string_view hook_id, F &&fn) | |
| 1281 | -> std::optional<std::invoke_result_t<F, MidHook &>> | ||
| 1282 | { | ||
| 1283 |
3/6std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 1 time.
|
3 | if (get_reentrancy_guard() > 0) |
| 1284 | { | ||
| 1285 | ✗ | m_logger.error("HookManager: Reentrant callback detected in try_with_mid_hook('{}')! " | |
| 1286 | "Callback holding m_hooks_mutex must not call HookManager mutators or teardown methods. " | ||
| 1287 | "Perform mutations outside the callback or use an asynchronous operation.", | ||
| 1288 | hook_id); | ||
| 1289 | ✗ | return std::nullopt; | |
| 1290 | } | ||
| 1291 | 3 | std::shared_lock<detail::SrwSharedMutex> lock(m_hooks_mutex, std::try_to_lock); | |
| 1292 |
3/6std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 1 time.
|
3 | if (!lock.owns_lock()) |
| 1293 | { | ||
| 1294 | ✗ | return std::nullopt; | |
| 1295 | } | ||
| 1296 | 3 | ReentrancyGuard guard(get_reentrancy_guard()); | |
| 1297 |
3/6std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 38 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 45 not taken.
|
3 | auto it = m_hooks.find(hook_id); |
| 1298 |
8/18std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 21 not taken.
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 31 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 25 taken 1 time.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 35 taken 1 time.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 25 not taken.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 1 time.
✗ Branch 26 → 35 not taken.
|
3 | if (it != m_hooks.end() && it->second->get_type() == HookType::Mid) |
| 1299 | { | ||
| 1300 |
2/6std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_Success_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 37 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_NotFound_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 43 not taken.
std::optional<std::invoke_result<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}, DetourModKit::MidHook&>::type> DetourModKit::HookManager::try_with_mid_hook<HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}>(std::basic_string_view<char, std::char_traits<char> >, HookManagerTest_TryWithMidHook_CallbackExecutesSuccessfully_Test::TestBody()::{lambda(DetourModKit::MidHook&)#1}&&):
✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 43 not taken.
|
4 | return std::invoke(std::forward<F>(fn), static_cast<MidHook &>(*it->second)); |
| 1301 | } | ||
| 1302 | 1 | return std::nullopt; | |
| 1303 | 3 | } | |
| 1304 | |||
| 1305 | private: | ||
| 1306 | /** @brief Internal log entry used to defer logging outside held locks. */ | ||
| 1307 | struct DeferredLogEntry | ||
| 1308 | { | ||
| 1309 | std::string msg; | ||
| 1310 | LogLevel level; | ||
| 1311 | }; | ||
| 1312 | explicit HookManager(Logger &logger = Logger::get_instance()); | ||
| 1313 | |||
| 1314 | mutable detail::SrwSharedMutex m_hooks_mutex; | ||
| 1315 | detail::HookMap m_hooks; | ||
| 1316 | detail::VmtHookMap m_vmt_hooks; | ||
| 1317 | |||
| 1318 | /** | ||
| 1319 | * @brief VMT hook names in creation order, maintained under m_hooks_mutex alongside m_vmt_hooks. | ||
| 1320 | * @details Bulk teardown walks this vector in reverse via clear_vmt_hooks_locked() so clones layered on the | ||
| 1321 | * same object are destroyed newest-first instead of in unordered_map bucket order. | ||
| 1322 | */ | ||
| 1323 | std::vector<std::string> m_vmt_creation_order; | ||
| 1324 | |||
| 1325 | /** | ||
| 1326 | * @brief Inline and mid hook names in creation order, maintained under m_hooks_mutex alongside m_hooks. | ||
| 1327 | * @details Teardown disables and destroys hooks by walking this vector in reverse (see | ||
| 1328 | * disable_hooks_reverse_order_locked() and clear_hooks_locked()) so hooks layered on one target | ||
| 1329 | * address unwind newest-first. SafetyHook saves the live prologue at create time, so a second hook on | ||
| 1330 | * a target stores "jmp -> first detour" as its own original bytes; restoring oldest-first would | ||
| 1331 | * rewrite the entry to jump into the older hook's about-to-be-freed trampoline. A single global | ||
| 1332 | * reverse walk yields the required per-address LIFO because the hooks on any one address are a | ||
| 1333 | * subsequence of the global creation order. | ||
| 1334 | */ | ||
| 1335 | std::vector<std::string> m_hook_creation_order; | ||
| 1336 | |||
| 1337 | Logger &m_logger; | ||
| 1338 | std::shared_ptr<safetyhook::Allocator> m_allocator; | ||
| 1339 | std::atomic<bool> m_shutdown_called{false}; | ||
| 1340 | |||
| 1341 | /** | ||
| 1342 | * @brief Gate that mutators (create_*_hook, enable_hook, disable_hook, remove_hook) acquire shared on entry, | ||
| 1343 | * allowing shutdown/remove_all_hooks to acquire exclusive to block new work. Teardown serialization uses | ||
| 1344 | * compare_exchange_strong on m_shutdown_called rather than a separate mutex. | ||
| 1345 | */ | ||
| 1346 | mutable detail::SrwSharedMutex m_mutator_gate; | ||
| 1347 | |||
| 1348 | /** | ||
| 1349 | * @brief Returns the thread-local reentrancy depth counter. | ||
| 1350 | * @details Declared const so the const read-only query accessors can consult the guard. The counter is | ||
| 1351 | * thread-local and independent of object state, so const is honest here. | ||
| 1352 | */ | ||
| 1353 | 31067 | [[nodiscard]] int &get_reentrancy_guard() const noexcept | |
| 1354 | { | ||
| 1355 | thread_local int reentrancy_counter{0}; | ||
| 1356 | 31067 | return reentrancy_counter; | |
| 1357 | } | ||
| 1358 | |||
| 1359 | struct ReentrancyGuard | ||
| 1360 | { | ||
| 1361 | int &counter; | ||
| 1362 | 36 | explicit ReentrancyGuard(int &cnt) noexcept : counter(cnt) { ++counter; } | |
| 1363 | 36 | ~ReentrancyGuard() noexcept { --counter; } | |
| 1364 | ReentrancyGuard(const ReentrancyGuard &) = delete; | ||
| 1365 | ReentrancyGuard &operator=(const ReentrancyGuard &) = delete; | ||
| 1366 | ReentrancyGuard(ReentrancyGuard &&) = delete; | ||
| 1367 | ReentrancyGuard &operator=(ReentrancyGuard &&) = delete; | ||
| 1368 | }; | ||
| 1369 | |||
| 1370 | // clang-format off | ||
| 1371 | /** | ||
| 1372 | * @brief Acquires m_hooks_mutex shared, or returns a disengaged lock when reentrant. | ||
| 1373 | * @details A with_* or try_with_* callback already holds m_hooks_mutex shared on this thread and has bumped the | ||
| 1374 | * reentrancy guard. Re-locking the non-recursive reader/writer mutex from the same thread is undefined | ||
| 1375 | * behavior and can deadlock if a writer is queued between the two acquisitions. A const query accessor | ||
| 1376 | * invoked from inside such a callback must read under the lock the callback already holds instead of | ||
| 1377 | * taking a second shared lock. When not reentrant, the returned lock owns m_hooks_mutex for the | ||
| 1378 | * caller's scope exactly as a direct shared_lock would. | ||
| 1379 | * @return An engaged shared_lock when guard == 0, otherwise a disengaged one. | ||
| 1380 | */ | ||
| 1381 | // clang-format on | ||
| 1382 | 29369 | [[nodiscard]] std::shared_lock<detail::SrwSharedMutex> lock_hooks_shared_reentrant() const | |
| 1383 | { | ||
| 1384 |
2/2✓ Branch 3 → 4 taken 5 times.
✓ Branch 3 → 5 taken 29372 times.
|
29369 | if (get_reentrancy_guard() > 0) |
| 1385 | { | ||
| 1386 | 5 | return std::shared_lock<detail::SrwSharedMutex>{}; | |
| 1387 | } | ||
| 1388 | 29372 | return std::shared_lock<detail::SrwSharedMutex>(m_hooks_mutex); | |
| 1389 | } | ||
| 1390 | |||
| 1391 | std::string error_to_string(const safetyhook::InlineHook::Error &err) const; | ||
| 1392 | std::string error_to_string(const safetyhook::MidHook::Error &err) const; | ||
| 1393 | |||
| 1394 | /** | ||
| 1395 | * @brief Enables or disables one already-located hook under the held locks. | ||
| 1396 | * @details Shared body of the batch toggle methods. The caller must hold m_mutator_gate and m_hooks_mutex (both | ||
| 1397 | * shared) and must have confirmed the manager is not shutting down. Logs the outcome exactly like the | ||
| 1398 | * single-hook enable_hook / disable_hook path. | ||
| 1399 | * @param hook_id The hook's name, used only for logging. | ||
| 1400 | * @param hook The hook to toggle. | ||
| 1401 | * @param enable true to enable, false to disable. | ||
| 1402 | * @param logs Sink for the outcome message; emitted after the locks release so no logger sink I/O happens | ||
| 1403 | * inside the critical section. | ||
| 1404 | * @return true if the hook is now in the requested state. | ||
| 1405 | */ | ||
| 1406 | [[nodiscard]] bool toggle_hook_locked(std::string_view hook_id, Hook &hook, bool enable, | ||
| 1407 | std::vector<DeferredLogEntry> &logs); | ||
| 1408 | |||
| 1409 | 131 | [[nodiscard]] bool hook_id_exists_locked(std::string_view hook_id) const | |
| 1410 | { | ||
| 1411 |
1/2✓ Branch 3 → 4 taken 131 times.
✗ Branch 3 → 8 not taken.
|
131 | return m_hooks.find(hook_id) != m_hooks.end(); |
| 1412 | } | ||
| 1413 | |||
| 1414 | 37 | [[nodiscard]] bool vmt_hook_exists_locked(std::string_view name) const | |
| 1415 | { | ||
| 1416 |
1/2✓ Branch 3 → 4 taken 37 times.
✗ Branch 3 → 8 not taken.
|
37 | return m_vmt_hooks.find(name) != m_vmt_hooks.end(); |
| 1417 | } | ||
| 1418 | |||
| 1419 | /** | ||
| 1420 | * @brief Destroys every VMT hook entry in reverse creation order and empties the registry. | ||
| 1421 | * @details When two hooks layer on the same object, the newer hook records the older hook's clone as its | ||
| 1422 | * "original" vtable. SafetyHook::VmtHook::destroy frees a hook's clone allocation unconditionally | ||
| 1423 | * but restores an object's vptr only when it still points at that hook's own clone, so destroying | ||
| 1424 | * the older hook first would leave the newer hook to restore the object's vptr to freed memory. | ||
| 1425 | * Newest-first destruction unwinds the layers so every restore writes a vptr that is still alive. | ||
| 1426 | * @note Must be called with m_hooks_mutex held exclusively. | ||
| 1427 | */ | ||
| 1428 | void clear_vmt_hooks_locked() noexcept; | ||
| 1429 | |||
| 1430 | /** | ||
| 1431 | * @brief Disables every inline and mid hook in reverse creation order. | ||
| 1432 | * @details Phase 1 of teardown. SafetyHook::InlineHook::disable() copies each hook's saved prologue bytes back | ||
| 1433 | * over the target; for hooks layered on one address the newer hook saved "jmp -> older detour" as its | ||
| 1434 | * original, so it must restore first (returning the prologue to that jump) before the older hook | ||
| 1435 | * restores the true original bytes. Disabling oldest-first would instead leave the live prologue | ||
| 1436 | * jumping into the older hook's trampoline, which clear_hooks_locked() then frees -- a use-after-free. | ||
| 1437 | * Hooks on distinct addresses are independent, so one global reverse-creation-order walk yields the | ||
| 1438 | * correct per-address LIFO for all of them at once. | ||
| 1439 | * @note Must be called with m_hooks_mutex held (shared is sufficient: this mutates Hook state, not the map). | ||
| 1440 | */ | ||
| 1441 | void disable_hooks_reverse_order_locked() noexcept; | ||
| 1442 | |||
| 1443 | /** | ||
| 1444 | * @brief Destroys every inline and mid hook in reverse creation order and empties the registry. | ||
| 1445 | * @details Phase 2 of teardown. By the time this runs disable_hooks_reverse_order_locked() has already restored | ||
| 1446 | * every prologue, so destruction only frees trampolines; freeing them newest-first keeps the order | ||
| 1447 | * uniform with the disable phase and with clear_vmt_hooks_locked() rather than depending on | ||
| 1448 | * unordered_map bucket order. | ||
| 1449 | * @note Must be called with m_hooks_mutex held exclusively. | ||
| 1450 | */ | ||
| 1451 | void clear_hooks_locked() noexcept; | ||
| 1452 | |||
| 1453 | /** | ||
| 1454 | * @brief Returns the name of the VMT hook whose cloned vptr matches @p vptr, or nullptr if none. | ||
| 1455 | * @details Walks the live VMT registry under the held m_hooks_mutex shared or exclusive lock and compares the | ||
| 1456 | * recorded cloned-vptr base. The check is O(N) over the (small) registry and is the only reliable way | ||
| 1457 | * to detect "object is already on a clone installed by this HookManager" without poking at | ||
| 1458 | * SafetyHook's private VmtHook layout. The caller-supplied @p vptr must already be plausibly a | ||
| 1459 | * userspace address; the comparison is a single qword, so no extra bounds work is needed here. | ||
| 1460 | * @note Must be called with m_hooks_mutex held (shared or exclusive). The read-only accessors that need the | ||
| 1461 | * reentrancy-aware lock use the same primitive. | ||
| 1462 | */ | ||
| 1463 | [[nodiscard]] const std::string *find_vmt_owner_of_vptr_locked(std::uintptr_t vptr) const noexcept; | ||
| 1464 | |||
| 1465 | /** | ||
| 1466 | * @brief Returns the managed hook installed at @p target_address, or nullptr if none. | ||
| 1467 | * @details Inline and mid hooks at the same address patch the same prologue bytes and share the teardown-order | ||
| 1468 | * hazard. The registry check is exact, unlike the prologue-byte heuristic used for foreign hooks. | ||
| 1469 | * @note Must be called with m_hooks_mutex held (shared or exclusive). | ||
| 1470 | */ | ||
| 1471 | [[nodiscard]] const std::string *find_hook_owner_of_target_locked(uintptr_t target_address) const noexcept; | ||
| 1472 | |||
| 1473 | /** | ||
| 1474 | * @brief Decodes the first few bytes of @p slot_value to decide if it looks like a callable function body. | ||
| 1475 | * @details Mirrors the inline pre-flight's detail::decode_* blacklist but is applied to a VMT slot value | ||
| 1476 | * directly so the VMT path does not need to round-trip through the scanner module. The first byte | ||
| 1477 | * is read with a single SEH-guarded byte load (no further decoding); when the first byte is one of | ||
| 1478 | * 0xCC/0xCD/0xC2/0xC3/0x00 the slot is rejected, when it is 0xEB/0xE9 the next 1-4 bytes are | ||
| 1479 | * inspected to see if the relative jump target is inside the same module per GetModuleHandleEx | ||
| 1480 | * HMODULE identity (heuristic for a jump stub). Everything else passes. Allocation-free, noexcept, | ||
| 1481 | * no logger dependency. | ||
| 1482 | * @param slot_value The first pointer-sized value read from the VMT slot. | ||
| 1483 | * @return true when the slot is a real function pointer (or a tail-call to outside the same module), | ||
| 1484 | * false when it is a breakpoint, bare RET, or same-module jump stub. | ||
| 1485 | */ | ||
| 1486 | [[nodiscard]] static bool looks_like_function_vmt_slot(std::uintptr_t slot_value) noexcept; | ||
| 1487 | }; | ||
| 1488 | |||
| 1489 | /** | ||
| 1490 | * @brief Convenience wrapper that installs an inline hook by direct address. | ||
| 1491 | * @details Forwards every argument to HookManager::create_inline_hook. Returns the registered hook name on success, | ||
| 1492 | * std::nullopt on failure. Diagnostic logging on failure is delegated to the underlying create_inline_hook | ||
| 1493 | * call, which already formats a richly-detailed Error line for every failure code; this wrapper does not | ||
| 1494 | * emit a duplicate. | ||
| 1495 | */ | ||
| 1496 | 3 | [[nodiscard]] inline std::optional<std::string> try_install_inline(std::string_view name, uintptr_t target_address, | |
| 1497 | void *detour_function, | ||
| 1498 | void **original_trampoline, | ||
| 1499 | const HookConfig &config = HookConfig()) | ||
| 1500 | { | ||
| 1501 |
1/2✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 15 not taken.
|
3 | auto result = HookManager::get_instance().create_inline_hook(name, target_address, detour_function, |
| 1502 |
1/2✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 15 not taken.
|
3 | original_trampoline, config); |
| 1503 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 9 taken 2 times.
|
3 | if (result) |
| 1504 | { | ||
| 1505 |
1/2✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 13 not taken.
|
1 | return *result; |
| 1506 | } | ||
| 1507 | 2 | return std::nullopt; | |
| 1508 | 3 | } | |
| 1509 | |||
| 1510 | /** | ||
| 1511 | * @brief Convenience wrapper that installs an inline hook by AOB scan. | ||
| 1512 | * @details Diagnostic logging on failure is delegated to the underlying create_inline_hook_aob call | ||
| 1513 | * (pattern-resolution failures and create_inline_hook failures both emit their own Error line), so this | ||
| 1514 | * wrapper does not emit a duplicate. The AOB scan is page-filtered exactly as create_inline_hook_aob | ||
| 1515 | * documents: a full SizeOfImage span containing a guard or no-access section is safe to pass. | ||
| 1516 | */ | ||
| 1517 | [[nodiscard]] inline std::optional<std::string> | ||
| 1518 | 1 | try_install_inline_aob(std::string_view name, uintptr_t module_base, size_t module_size, | |
| 1519 | std::string_view aob_pattern, std::ptrdiff_t aob_offset, void *detour_function, | ||
| 1520 | void **original_trampoline, const HookConfig &config = HookConfig()) | ||
| 1521 | { | ||
| 1522 |
1/2✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 15 not taken.
|
1 | auto result = HookManager::get_instance().create_inline_hook_aob( |
| 1523 |
1/2✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 15 not taken.
|
1 | name, module_base, module_size, aob_pattern, aob_offset, detour_function, original_trampoline, config); |
| 1524 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 1 time.
|
1 | if (result) |
| 1525 | { | ||
| 1526 | ✗ | return *result; | |
| 1527 | } | ||
| 1528 | 1 | return std::nullopt; | |
| 1529 | 1 | } | |
| 1530 | |||
| 1531 | /** | ||
| 1532 | * @brief Convenience wrapper that installs a mid-function hook by direct address. | ||
| 1533 | * @details Diagnostic logging on failure is delegated to the underlying create_mid_hook call. | ||
| 1534 | */ | ||
| 1535 | 1 | [[nodiscard]] inline std::optional<std::string> try_install_mid(std::string_view name, uintptr_t target_address, | |
| 1536 | safetyhook::MidHookFn detour_function, | ||
| 1537 | const HookConfig &config = HookConfig()) | ||
| 1538 | { | ||
| 1539 |
2/4✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 15 not taken.
✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 15 not taken.
|
1 | auto result = HookManager::get_instance().create_mid_hook(name, target_address, detour_function, config); |
| 1540 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 1 time.
|
1 | if (result) |
| 1541 | { | ||
| 1542 | ✗ | return *result; | |
| 1543 | } | ||
| 1544 | 1 | return std::nullopt; | |
| 1545 | 1 | } | |
| 1546 | |||
| 1547 | /** | ||
| 1548 | * @brief Convenience wrapper that installs a mid-function hook by AOB scan. | ||
| 1549 | * @details Diagnostic logging on failure is delegated to the underlying create_mid_hook_aob call. The AOB scan is | ||
| 1550 | * page-filtered exactly as create_mid_hook_aob documents: a full SizeOfImage span containing a guard or | ||
| 1551 | * no-access section is safe to pass. | ||
| 1552 | */ | ||
| 1553 | [[nodiscard]] inline std::optional<std::string> | ||
| 1554 | 1 | try_install_mid_aob(std::string_view name, uintptr_t module_base, size_t module_size, std::string_view aob_pattern, | |
| 1555 | std::ptrdiff_t aob_offset, safetyhook::MidHookFn detour_function, | ||
| 1556 | const HookConfig &config = HookConfig()) | ||
| 1557 | { | ||
| 1558 |
1/2✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 15 not taken.
|
1 | auto result = HookManager::get_instance().create_mid_hook_aob(name, module_base, module_size, aob_pattern, |
| 1559 |
1/2✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 15 not taken.
|
1 | aob_offset, detour_function, config); |
| 1560 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 9 taken 1 time.
|
1 | if (result) |
| 1561 | { | ||
| 1562 | ✗ | return *result; | |
| 1563 | } | ||
| 1564 | 1 | return std::nullopt; | |
| 1565 | 1 | } | |
| 1566 | } // namespace DetourModKit | ||
| 1567 | |||
| 1568 | #endif // DETOURMODKIT_HOOK_MANAGER_HPP | ||
| 1569 |